From e8a13edde7c0ba2ef80344ab7c7288e7bb2eb6b5 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 21 Feb 2026 05:50:24 +0100 Subject: [PATCH 01/16] fix: add created_by ownership check to update/delete saved inputs (#8038) * fix: add created_by ownership check to update/delete saved inputs Co-Authored-By: Claude Opus 4.6 * all --------- Co-authored-by: Claude Opus 4.6 --- backend/windmill-api-inputs/src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/windmill-api-inputs/src/lib.rs b/backend/windmill-api-inputs/src/lib.rs index 99ba8d2bfe..2973085fe1 100644 --- a/backend/windmill-api-inputs/src/lib.rs +++ b/backend/windmill-api-inputs/src/lib.rs @@ -6,7 +6,6 @@ * LICENSE-AGPL for a copy of the license. */ -use windmill_api_auth::ApiAuthed; use axum::{ extract::{Path, Query}, routing::{get, post}, @@ -20,6 +19,7 @@ use std::{ fmt::{Display, Formatter}, vec, }; +use windmill_api_auth::ApiAuthed; use windmill_common::{ db::UserDB, error::JsonResult, @@ -352,11 +352,12 @@ async fn update_input( ) -> JsonResult { let mut tx = user_db.begin(&authed).await?; - sqlx::query("UPDATE input SET name = $1, is_public = $2 WHERE id = $3 and workspace_id = $4") + sqlx::query("UPDATE input SET name = $1, is_public = $2 WHERE id = $3 and workspace_id = $4 AND created_by = $5") .bind(&input.name) .bind(&input.is_public) .bind(&input.id) .bind(&w_id) + .bind(&authed.username) .execute(&mut *tx) .await?; @@ -372,9 +373,10 @@ async fn delete_input( ) -> JsonResult { let mut tx = user_db.begin(&authed).await?; - sqlx::query("DELETE FROM input WHERE id = $1 and workspace_id = $2") + sqlx::query("DELETE FROM input WHERE id = $1 and workspace_id = $2 AND created_by = $3") .bind(&i_id) .bind(&w_id) + .bind(&authed.username) .execute(&mut *tx) .await?; From 9eb15312f663aa6d700e8ac562d7b5c75c2221f7 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 21 Feb 2026 06:29:36 +0100 Subject: [PATCH 02/16] feat: add .npmrc support for private npm registries (#8039) * feat: add .npmrc support for private npm registries Add a new `npmrc` instance setting that accepts full .npmrc file content for configuring private npm registries. Works with bun (native .npmrc support since 1.1.18), deno (native .npmrc support in 2.x), and the npm proxy (parses default registry + auth token from .npmrc). Legacy `npm_config_registry` and `bunfig_install_scopes` fields are now hidden when empty, so new users only see the .npmrc field. Also fixes a pre-existing race condition where gen_bunfig was called after start_child_process. Co-Authored-By: Claude Opus 4.6 * all --------- Co-authored-by: Claude Opus 4.6 --- .github/workflows/backend-test.yml | 6 + backend/Cargo.lock | 1 + backend/src/monitor.rs | 13 +- backend/tests/bun_jobs.rs | 217 +++++++++++++----- backend/tests/worker.rs | 60 +++++ backend/windmill-api-npm-proxy/Cargo.toml | 1 + backend/windmill-api-npm-proxy/src/lib.rs | 116 +++++----- .../windmill-common/src/global_settings.rs | 1 + .../windmill-common/src/instance_config.rs | 20 +- backend/windmill-common/src/utils.rs | 93 ++++++++ backend/windmill-worker/src/bun_executor.rs | 98 +++++--- backend/windmill-worker/src/deno_executor.rs | 51 ++-- backend/windmill-worker/src/worker.rs | 1 + frontend/package-lock.json | 46 +--- .../src/lib/components/instanceSettings.ts | 30 ++- 15 files changed, 527 insertions(+), 227 deletions(-) diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml index a2eeaaa044..48bd1e11ec 100644 --- a/.github/workflows/backend-test.yml +++ b/.github/workflows/backend-test.yml @@ -165,6 +165,12 @@ jobs: fi echo "NPM_TOKEN=${NPM_TOKEN}" >> $GITHUB_ENV + { + echo "TEST_NPMRC<> $GITHUB_ENV echo "Got NPM token successfully: ${NPM_TOKEN:0:10}..." # Configure npm globally with the auth token diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 513e32ed2f..a221bb6fa9 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -16200,6 +16200,7 @@ version = "1.640.0" dependencies = [ "axum 0.7.9", "flate2", + "reqwest 0.13.1", "serde", "serde_json", "sqlx", diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 75cff8971f..17e79c3393 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -54,7 +54,7 @@ use windmill_common::{ HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, - NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING, + NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING, OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING, @@ -89,9 +89,9 @@ use windmill_worker::{ result_processor::handle_job_error, JobCompletedSender, JobIsolationLevel, OtelTracingProxySettings, SameWorkerSender, BUNFIG_INSTALL_SCOPES, CARGO_REGISTRIES, INSTANCE_PYTHON_VERSION, JAVA_HOME_DIR, JOB_DEFAULT_TIMEOUT, JOB_ISOLATION, KEEP_JOB_DIR, - MAVEN_REPOS, MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPM_CONFIG_REGISTRY, NSJAIL_AVAILABLE, - NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, - POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL, UV_INDEX_STRATEGY, + MAVEN_REPOS, MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPMRC, NPM_CONFIG_REGISTRY, + NSJAIL_AVAILABLE, NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, + PIP_INDEX_URL, POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL, UV_INDEX_STRATEGY, }; #[cfg(feature = "parquet")] @@ -330,6 +330,7 @@ pub async fn initial_load( reload_uv_index_strategy_setting(&conn).await; reload_npm_config_registry_setting(&conn).await; reload_bunfig_install_scopes_setting(&conn).await; + reload_npmrc_setting(&conn).await; reload_instance_python_version_setting(&conn).await; reload_nuget_config_setting(&conn).await; reload_powershell_repo_url_setting(&conn).await; @@ -1306,6 +1307,10 @@ pub async fn reload_bunfig_install_scopes_setting(conn: &Connection) { .await; } +pub async fn reload_npmrc_setting(conn: &Connection) { + reload_option_setting_with_tracing(conn, NPMRC_SETTING, "NPMRC", NPMRC.clone()).await; +} + pub async fn reload_nuget_config_setting(conn: &Connection) { reload_option_setting_with_tracing( conn, diff --git a/backend/tests/bun_jobs.rs b/backend/tests/bun_jobs.rs index 534d530095..15a2b3c27c 100644 --- a/backend/tests/bun_jobs.rs +++ b/backend/tests/bun_jobs.rs @@ -1,8 +1,8 @@ -use windmill_test_utils::*; use sqlx::postgres::Postgres; use sqlx::Pool; use windmill_common::jobs::{JobPayload, RawCode}; use windmill_common::scripts::ScriptLang; +use windmill_test_utils::*; // ============================================================================ // Basic Execution Tests @@ -27,8 +27,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -63,8 +63,8 @@ export function main(name: string, count: number) { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -104,8 +104,9 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -135,8 +136,9 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -167,8 +169,9 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -207,8 +210,8 @@ export async function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -245,8 +248,9 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -276,8 +280,9 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -318,8 +323,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -358,8 +363,8 @@ export function notMain() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -398,8 +403,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -437,8 +442,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -474,8 +479,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -516,8 +521,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -613,8 +618,9 @@ export function main() { path: Some("f/nested/test_deep".to_string()), language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -647,8 +653,9 @@ export function main() { path: Some("f/nested/test_deep_relative".to_string()), language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -693,8 +700,8 @@ export function main() { path: Some("f/circular/test_both".to_string()), language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -741,8 +748,8 @@ export function main(x: number) { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -791,8 +798,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -836,8 +843,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -859,11 +866,11 @@ export function main() { // ============================================================================ mod dedicated_worker_protocol { - use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult}; use std::io::{BufRead, BufReader, Write}; use std::process::{Command, Stdio}; + use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult}; use windmill_worker::{ - build_loader, generate_dedicated_worker_wrapper, BUN_DEDICATED_WORKER_ARGS, LoaderMode, + build_loader, generate_dedicated_worker_wrapper, LoaderMode, BUN_DEDICATED_WORKER_ARGS, BUN_PATH, NODE_BIN_PATH, }; @@ -934,12 +941,8 @@ mod dedicated_worker_protocol { let temp_dir = tempfile::tempdir().unwrap(); // Create files and get the wrapper path (bundled for node, raw for bun) - let wrapper_path = create_test_worker_files( - temp_dir.path(), - script, - arg_names, - runtime == "node", - ); + let wrapper_path = + create_test_worker_files(temp_dir.path(), script, arg_names, runtime == "node"); let wrapper_str = wrapper_path.to_str().unwrap(); // Build args matching production behavior @@ -992,7 +995,10 @@ mod dedicated_worker_protocol { match parse_dedicated_worker_line(response.trim()) { DedicatedWorkerResult::Success(value) => results.push(Ok(value)), DedicatedWorkerResult::Error(err) => { - let msg = err["message"].as_str().unwrap_or("Unknown error").to_string(); + let msg = err["message"] + .as_str() + .unwrap_or("Unknown error") + .to_string(); results.push(Err(msg)); } other => panic!("Unexpected response: {:?}", other), @@ -1162,8 +1168,8 @@ export function main(name: string) { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -1190,6 +1196,68 @@ export function main(name: string) { Ok(()) } +/// Test that full .npmrc content works for bun jobs with private registries. +/// Requires: +/// - `TEST_NPMRC` environment variable set to the full .npmrc content +#[cfg(feature = "private_registry_test")] +#[sqlx::test(fixtures("base"))] +async fn test_bun_job_private_npmrc(db: Pool) -> anyhow::Result<()> { + use windmill_worker::NPMRC; + + let npmrc_content = std::env::var("TEST_NPMRC") + .expect("TEST_NPMRC must be set when running private_registry_test"); + + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + { + let mut npmrc = NPMRC.write().await; + *npmrc = Some(npmrc_content.clone()); + } + + let content = r#" +import { greet } from "@windmill-test/private-pkg"; + +export function main(name: string) { + return greet(name); +} +"# + .to_owned(); + + let job = JobPayload::Code(RawCode { + hash: None, + content, + path: None, + language: ScriptLang::Bun, + lock: None, + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + }); + + let result = RunJob::from(job) + .arg("name", serde_json::json!("World")) + .run_until_complete(&db, false, port) + .await + .json_result() + .unwrap(); + + { + let mut npmrc = NPMRC.write().await; + *npmrc = None; + } + + assert_eq!( + result, + serde_json::json!("Hello from private package, World!") + ); + Ok(()) +} + /// Tests for RELATIVE_BUN_BUILDER (loader_builder.bun.js) /// These tests verify Bun's behavior for import scanning and package.json generation. /// Purpose: Catch regressions when upgrading Bun versions. @@ -1241,8 +1309,8 @@ mod bun_builder_tests { } // Read generated package.json - let package_json = std::fs::read_to_string(dir.join("package.json")) - .expect("package.json not generated"); + let package_json = + std::fs::read_to_string(dir.join("package.json")).expect("package.json not generated"); serde_json::from_str(&package_json).expect("Invalid JSON in package.json") } @@ -1257,7 +1325,10 @@ export function main() { return lodash; } let pkg = run_builder(main_ts); let deps = pkg["dependencies"].as_object().unwrap(); - assert!(deps.contains_key("lodash"), "lodash should be in dependencies"); + assert!( + deps.contains_key("lodash"), + "lodash should be in dependencies" + ); assert_eq!(deps["lodash"], "latest"); } @@ -1271,7 +1342,10 @@ export function main() { return _; } let pkg = run_builder(main_ts); let deps = pkg["dependencies"].as_object().unwrap(); - assert!(deps.contains_key("lodash"), "lodash should be in dependencies"); + assert!( + deps.contains_key("lodash"), + "lodash should be in dependencies" + ); assert_eq!(deps["lodash"], "4.17.21"); } @@ -1304,9 +1378,18 @@ export function main() { return { lodash, axios, dayjs }; } let pkg = run_builder(main_ts); let deps = pkg["dependencies"].as_object().unwrap(); - assert!(deps.contains_key("lodash"), "lodash should be in dependencies"); - assert!(deps.contains_key("axios"), "axios should be in dependencies"); - assert!(deps.contains_key("dayjs"), "dayjs should be in dependencies"); + assert!( + deps.contains_key("lodash"), + "lodash should be in dependencies" + ); + assert!( + deps.contains_key("axios"), + "axios should be in dependencies" + ); + assert!( + deps.contains_key("dayjs"), + "dayjs should be in dependencies" + ); assert_eq!(deps.len(), 3, "Should have exactly 3 dependencies"); } @@ -1330,8 +1413,15 @@ export function main() { return { fs, path, lodash }; } !deps.contains_key("path"), "path (builtin) should NOT be in dependencies" ); - assert!(deps.contains_key("lodash"), "lodash should be in dependencies"); - assert_eq!(deps.len(), 1, "Should have exactly 1 dependency (lodash only)"); + assert!( + deps.contains_key("lodash"), + "lodash should be in dependencies" + ); + assert_eq!( + deps.len(), + 1, + "Should have exactly 1 dependency (lodash only)" + ); } /// Test: semver.order() resolves version conflicts (picks lowest version) @@ -1347,7 +1437,10 @@ export function main() { return { a, b }; } let pkg = run_builder(main_ts); let deps = pkg["dependencies"].as_object().unwrap(); - assert!(deps.contains_key("lodash"), "lodash should be in dependencies"); + assert!( + deps.contains_key("lodash"), + "lodash should be in dependencies" + ); // The builder sorts by semver and picks the first (lowest) version assert_eq!( deps["lodash"], "4.17.10", diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index d084a0177c..647647604f 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -1592,6 +1592,66 @@ export async function main(a: Date) { Ok(()) } +/// Test that full .npmrc content works for deno jobs with private registries. +/// Requires: +/// - `TEST_NPMRC` environment variable set to the full .npmrc content +#[cfg(feature = "private_registry_test")] +#[sqlx::test(fixtures("base"))] +async fn test_deno_job_private_npmrc(db: Pool) -> anyhow::Result<()> { + use windmill_worker::NPMRC; + + let npmrc_content = std::env::var("TEST_NPMRC") + .expect("TEST_NPMRC must be set when running private_registry_test"); + + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + { + let mut npmrc = NPMRC.write().await; + *npmrc = Some(npmrc_content.clone()); + } + + let content = r#" +import { greet } from "npm:@windmill-test/private-pkg"; + +export function main(name: string) { + return greet(name); +} +"# + .to_owned(); + + let result = RunJob::from(JobPayload::Code(RawCode { + hash: None, + content, + path: None, + language: ScriptLang::Deno, + lock: None, + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + })) + .arg("name", json!("World")) + .run_until_complete(&db, false, port) + .await + .json_result() + .unwrap(); + + { + let mut npmrc = NPMRC.write().await; + *npmrc = None; + } + + assert_eq!( + result, + serde_json::json!("Hello from private package, World!") + ); + Ok(()) +} + #[cfg(feature = "python")] #[sqlx::test(fixtures("base"))] async fn test_python_job_datetime_and_bytes(db: Pool) -> anyhow::Result<()> { diff --git a/backend/windmill-api-npm-proxy/Cargo.toml b/backend/windmill-api-npm-proxy/Cargo.toml index c5852090ce..4cdae773fe 100644 --- a/backend/windmill-api-npm-proxy/Cargo.toml +++ b/backend/windmill-api-npm-proxy/Cargo.toml @@ -13,6 +13,7 @@ windmill-api-auth.workspace = true windmill-common = { workspace = true, default-features = false } axum.workspace = true flate2.workspace = true +reqwest.workspace = true serde.workspace = true serde_json.workspace = true sqlx.workspace = true diff --git a/backend/windmill-api-npm-proxy/src/lib.rs b/backend/windmill-api-npm-proxy/src/lib.rs index 6bcffe0d0a..310142598f 100644 --- a/backend/windmill-api-npm-proxy/src/lib.rs +++ b/backend/windmill-api-npm-proxy/src/lib.rs @@ -14,8 +14,10 @@ use std::collections::HashMap; use tower_http::cors::{Any, CorsLayer}; use windmill_common::{ error::{Error, JsonResult, Result}, - global_settings::{load_value_from_global_settings, NPM_CONFIG_REGISTRY_SETTING}, - utils::StripPath, + global_settings::{ + load_value_from_global_settings, NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING, + }, + utils::{parse_npmrc_registry, StripPath}, }; use windmill_api_auth::ApiAuthed; @@ -129,6 +131,14 @@ pub fn workspaced_service() -> Router { ) } +fn build_registry_request(url: &str, auth_token: &Option) -> reqwest::RequestBuilder { + let mut req = HTTP_CLIENT.get(url); + if let Some(token) = auth_token { + req = req.bearer_auth(token); + } + req +} + /// Get package metadata (versions and tags) from the private registry async fn get_package_metadata( _authed: ApiAuthed, @@ -136,21 +146,14 @@ async fn get_package_metadata( Extension(db): Extension>, ) -> JsonResult { let package = parse_package_name(package_path.to_path()); - let npm_registry = get_npm_registry(&db).await?; - - if npm_registry.is_none() { - return Err(Error::BadRequest( - "No private npm registry configured".to_string(), - )); - } - - let registry_url = npm_registry.unwrap(); + let (registry_url, auth_token) = get_npm_registry(&db) + .await? + .ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?; let package_url = format_registry_url(®istry_url, &package, None, None); tracing::info!("Fetching package metadata from: {}", package_url); - let response = HTTP_CLIENT - .get(&package_url) + let response = build_registry_request(&package_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?; @@ -167,7 +170,6 @@ async fn get_package_metadata( .await .map_err(|e| Error::InternalErr(format!("Failed to parse package metadata: {}", e)))?; - // Extract versions and dist-tags from the package metadata let mut versions = Vec::new(); let mut tags = HashMap::new(); @@ -194,22 +196,15 @@ async fn resolve_package_version( Extension(db): Extension>, ) -> JsonResult { let package = parse_package_name(package_path.to_path()); - let npm_registry = get_npm_registry(&db).await?; - - if npm_registry.is_none() { - return Err(Error::BadRequest( - "No private npm registry configured".to_string(), - )); - } - - let registry_url = npm_registry.unwrap(); + let (registry_url, auth_token) = get_npm_registry(&db) + .await? + .ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?; let reference = query.tag.unwrap_or_else(|| "latest".to_string()); let package_url = format_registry_url(®istry_url, &package, None, None); tracing::info!("Resolving package version from: {}", package_url); - let response = HTTP_CLIENT - .get(&package_url) + let response = build_registry_request(&package_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?; @@ -256,21 +251,14 @@ async fn get_package_filetree( Extension(db): Extension>, ) -> JsonResult { let (package, version) = parse_package_and_version(package_version_path.to_path())?; - let npm_registry = get_npm_registry(&db).await?; - - if npm_registry.is_none() { - return Err(Error::BadRequest( - "No private npm registry configured".to_string(), - )); - } - - let registry_url = npm_registry.unwrap(); + let (registry_url, auth_token) = get_npm_registry(&db) + .await? + .ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?; let package_url = format_registry_url(®istry_url, &package, None, None); tracing::info!("Fetching package filetree from: {}", package_url); - let response = HTTP_CLIENT - .get(&package_url) + let response = build_registry_request(&package_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?; @@ -287,7 +275,6 @@ async fn get_package_filetree( .await .map_err(|e| Error::InternalErr(format!("Failed to parse package metadata: {}", e)))?; - // Get the tarball URL for this version let tarball_url = package_json .get("versions") .and_then(|v| v.get(&version)) @@ -296,9 +283,7 @@ async fn get_package_filetree( .and_then(|t| t.as_str()) .ok_or_else(|| Error::NotFound(format!("Tarball not found for {}@{}", package, version)))?; - // Download and extract tarball to get file list - let tarball_response = HTTP_CLIENT - .get(tarball_url) + let tarball_response = build_registry_request(tarball_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to download tarball: {}", e)))?; @@ -337,21 +322,14 @@ async fn get_package_file( Extension(db): Extension>, ) -> Result { let (package, version, filepath) = parse_package_version_and_file(full_path.to_path())?; - let npm_registry = get_npm_registry(&db).await?; - - if npm_registry.is_none() { - return Err(Error::BadRequest( - "No private npm registry configured".to_string(), - )); - } - - let registry_url = npm_registry.unwrap(); + let (registry_url, auth_token) = get_npm_registry(&db) + .await? + .ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?; let package_url = format_registry_url(®istry_url, &package, None, None); tracing::info!("Fetching package file from: {}", package_url); - let response = HTTP_CLIENT - .get(&package_url) + let response = build_registry_request(&package_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?; @@ -368,7 +346,6 @@ async fn get_package_file( .await .map_err(|e| Error::InternalErr(format!("Failed to parse package metadata: {}", e)))?; - // Get the tarball URL for this version let tarball_url = package_json .get("versions") .and_then(|v| v.get(&version)) @@ -377,9 +354,7 @@ async fn get_package_file( .and_then(|t| t.as_str()) .ok_or_else(|| Error::NotFound(format!("Tarball not found for {}@{}", package, version)))?; - // Download tarball - let tarball_response = HTTP_CLIENT - .get(tarball_url) + let tarball_response = build_registry_request(tarball_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to download tarball: {}", e)))?; @@ -402,13 +377,38 @@ async fn get_package_file( Ok(file_content) } -/// Get the npm registry URL from global settings -async fn get_npm_registry(db: &sqlx::Pool) -> Result> { +/// Get the npm registry URL and optional auth token from global settings. +/// Checks the `npmrc` setting first, then falls back to `npm_config_registry`. +async fn get_npm_registry( + db: &sqlx::Pool, +) -> Result)>> { + let npmrc = load_value_from_global_settings(db, NPMRC_SETTING) + .await? + .and_then(|v| v.as_str().map(|s| s.to_string())); + + if let Some(ref npmrc_content) = npmrc { + if let Some(parsed) = parse_npmrc_registry(npmrc_content) { + return Ok(Some(parsed)); + } + } + let registry = load_value_from_global_settings(db, NPM_CONFIG_REGISTRY_SETTING) .await? .and_then(|v| v.as_str().map(|s| s.to_string())); - Ok(registry) + if let Some(ref s) = registry { + let (url, token) = if s.contains(":_authToken=") { + let parts: Vec<&str> = s.split(":_authToken=").collect(); + let url = parts[0].to_string(); + let token = parts.get(1).map(|t| t.to_string()); + (url, token) + } else { + (s.clone(), None) + }; + return Ok(Some((url, token))); + } + + Ok(None) } /// Format a registry URL for a package diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index 712ae50098..3347127303 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -10,6 +10,7 @@ pub const REQUEST_SIZE_LIMIT_SETTING: &str = "request_size_limit_mb"; pub const LICENSE_KEY_SETTING: &str = "license_key"; pub const NPM_CONFIG_REGISTRY_SETTING: &str = "npm_config_registry"; pub const BUNFIG_INSTALL_SCOPES_SETTING: &str = "bunfig_install_scopes"; +pub const NPMRC_SETTING: &str = "npmrc"; pub const NUGET_CONFIG_SETTING: &str = "nuget_config"; pub const POWERSHELL_REPO_URL_SETTING: &str = "powershell_repo_url"; pub const POWERSHELL_REPO_PAT_SETTING: &str = "powershell_repo_pat"; diff --git a/backend/windmill-common/src/instance_config.rs b/backend/windmill-common/src/instance_config.rs index 7270577e88..89df71824f 100644 --- a/backend/windmill-common/src/instance_config.rs +++ b/backend/windmill-common/src/instance_config.rs @@ -261,6 +261,8 @@ pub struct GlobalSettings { #[serde(skip_serializing_if = "Option::is_none")] pub bunfig_install_scopes: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub npmrc: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub nuget_config: Option, #[serde(skip_serializing_if = "Option::is_none")] pub maven_repos: Option, @@ -774,7 +776,11 @@ pub const PROTECTED_SETTINGS: &[&str] = &[ /// Note: jwt_secret is intentionally NOT hidden — it is included in YAML exports so that /// operators can set it via ConfigMap. It is protected from deletion (PROTECTED_SETTINGS) /// and from being set to empty/null, and its value is partially redacted in log output. -pub const HIDDEN_SETTINGS: &[&str] = &["uid", "min_keep_alive_version", "automate_username_creation"]; +pub const HIDDEN_SETTINGS: &[&str] = &[ + "uid", + "min_keep_alive_version", + "automate_username_creation", +]; /// Top-level settings whose entire value is sensitive and must be fully redacted in logs. const SENSITIVE_SETTINGS: &[&str] = &[ @@ -788,6 +794,7 @@ const SENSITIVE_SETTINGS: &[&str] = &[ "pip_extra_index_url", "npm_config_registry", "bunfig_install_scopes", + "npmrc", "maven_repos", "ruby_repos", "powershell_repo_pat", @@ -798,7 +805,10 @@ const SENSITIVE_SETTINGS: &[&str] = &[ const NESTED_SENSITIVE_FIELDS: &[(&str, &[&str])] = &[ ("smtp_settings", &["smtp_password"]), ("secret_backend", &["token"]), - ("object_store_cache_config", &["secret_key", "serviceAccountKey"]), + ( + "object_store_cache_config", + &["secret_key", "serviceAccountKey"], + ), ]; fn redact_json_value(value: &serde_json::Value) -> serde_json::Value { @@ -2353,7 +2363,11 @@ mod tests { ); let diff = diff_global_settings(¤t, &desired, ApplyMode::Merge); - assert_eq!(diff.upserts.len(), 1, "Same client with newer expiry should update even with different signature"); + assert_eq!( + diff.upserts.len(), + 1, + "Same client with newer expiry should update even with different signature" + ); } #[test] diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index 0c5f4b4a23..2fec2f9e94 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -1281,3 +1281,96 @@ mod tests { assert_eq!(parsed, serde_json::json!([[1], [2], [3], [4], [5]])); } } + +/// Parse .npmrc content to extract the default registry URL and its auth token. +/// Returns `Some((registry_url, Option))` if a default registry is found. +pub fn parse_npmrc_registry(npmrc_content: &str) -> Option<(String, Option)> { + let mut registry_url: Option = None; + let mut auth_tokens: Vec<(String, String)> = Vec::new(); + + for line in npmrc_content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with(';') { + continue; + } + + if let Some(url) = line.strip_prefix("registry=") { + registry_url = Some(url.trim().to_string()); + } + + if line.starts_with("//") { + if let Some((prefix, token)) = line.split_once(":_authToken=") { + auth_tokens.push((prefix.to_string(), token.to_string())); + } + } + } + + let url = registry_url?; + let url_without_protocol = url.trim_start_matches("https:").trim_start_matches("http:"); + let url_prefix = url_without_protocol.trim_end_matches('/'); + + let token = auth_tokens + .iter() + .find(|(prefix, _)| { + let p = prefix.trim_end_matches('/'); + p == url_prefix + }) + .map(|(_, token)| token.clone()); + + Some((url, token)) +} + +#[cfg(test)] +mod npmrc_tests { + use super::parse_npmrc_registry; + + #[test] + fn test_parse_simple_registry() { + let npmrc = "registry=https://registry.mycompany.com/\n//registry.mycompany.com/:_authToken=secret123\n"; + let result = parse_npmrc_registry(npmrc); + assert_eq!( + result, + Some(( + "https://registry.mycompany.com/".to_string(), + Some("secret123".to_string()) + )) + ); + } + + #[test] + fn test_parse_registry_without_auth() { + let npmrc = "registry=https://registry.npmjs.org/\n"; + let result = parse_npmrc_registry(npmrc); + assert_eq!( + result, + Some(("https://registry.npmjs.org/".to_string(), None)) + ); + } + + #[test] + fn test_parse_scoped_only_no_default() { + let npmrc = + "@myorg:registry=https://registry.myorg.com/\n//registry.myorg.com/:_authToken=tok\n"; + let result = parse_npmrc_registry(npmrc); + assert_eq!(result, None); + } + + #[test] + fn test_parse_with_comments() { + let npmrc = "# My registry\nregistry=https://r.example.com/\n; auth\n//r.example.com/:_authToken=tok\n"; + let result = parse_npmrc_registry(npmrc); + assert_eq!( + result, + Some(( + "https://r.example.com/".to_string(), + Some("tok".to_string()) + )) + ); + } + + #[test] + fn test_parse_empty_npmrc() { + assert_eq!(parse_npmrc_registry(""), None); + assert_eq!(parse_npmrc_registry("# just a comment"), None); + } +} diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs index 64c434db04..9c46db572d 100644 --- a/backend/windmill-worker/src/bun_executor.rs +++ b/backend/windmill-worker/src/bun_executor.rs @@ -22,8 +22,8 @@ use crate::{ handle_child::handle_child, is_sandboxing_enabled, read_ee_registry, BUNFIG_INSTALL_SCOPES, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, BUN_NO_CACHE, BUN_PATH, DISABLE_NUSER, HOME_ENV, NODE_BIN_PATH, NODE_PATH, - NPM_CONFIG_REGISTRY, NPM_PATH, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, TRACING_PROXY_CA_CERT_PATH, - TZ_ENV, + NPMRC, NPM_CONFIG_REGISTRY, NPM_PATH, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, + TRACING_PROXY_CA_CERT_PATH, TZ_ENV, }; use windmill_common::{ client::AuthedClient, @@ -299,6 +299,20 @@ async fn gen_bunfig( w_id: &str, db: Option<&Connection>, ) -> Result<()> { + let npmrc = if let Some(conn) = db { + read_ee_registry(NPMRC.read().await.clone(), "npmrc", job_id, w_id, conn).await + } else { + NPMRC.read().await.clone() + }; + + if let Some(ref npmrc_content) = npmrc { + if !npmrc_content.trim().is_empty() { + tracing::debug!("Writing .npmrc for bun from npmrc setting"); + write_file(job_dir, ".npmrc", npmrc_content)?; + return Ok(()); + } + } + let (registry, bunfig_install_scopes) = if let Some(conn) = db { ( read_ee_registry( @@ -402,39 +416,55 @@ pub async fn install_bun_lockfile( }; let has_file = if npm_mode { - let registry = if let Some(conn) = db { - read_ee_registry( - NPM_CONFIG_REGISTRY.read().await.clone(), - "npm registry", - job_id, - w_id, - conn, - ) - .await + let npmrc = if let Some(conn) = db { + read_ee_registry(NPMRC.read().await.clone(), "npmrc", job_id, w_id, conn).await } else { - NPM_CONFIG_REGISTRY.read().await.clone() + NPMRC.read().await.clone() }; - if let Some(registry) = registry { - let content = registry - .trim_start_matches("https:") - .trim_start_matches("http:"); - let mut splitted = registry.split(":_authToken="); - let custom_registry = splitted.next().unwrap_or_default(); - npm_logs.push_str(&format!( - "Using custom npm registry: {custom_registry} {}\n", - if splitted.next().is_some() { - "with authToken" - } else { - "without authToken" - } - )); - - child_cmd.env("NPM_CONFIG_REGISTRY", custom_registry); - write_file(job_dir, ".npmrc", content)?; - true + if let Some(ref npmrc_content) = npmrc { + if !npmrc_content.trim().is_empty() { + npm_logs.push_str("Using .npmrc from instance settings\n"); + write_file(job_dir, ".npmrc", npmrc_content)?; + true + } else { + false + } } else { - false + let registry = if let Some(conn) = db { + read_ee_registry( + NPM_CONFIG_REGISTRY.read().await.clone(), + "npm registry", + job_id, + w_id, + conn, + ) + .await + } else { + NPM_CONFIG_REGISTRY.read().await.clone() + }; + if let Some(registry) = registry { + let content = registry + .trim_start_matches("https:") + .trim_start_matches("http:"); + + let mut splitted = registry.split(":_authToken="); + let custom_registry = splitted.next().unwrap_or_default(); + npm_logs.push_str(&format!( + "Using custom npm registry: {custom_registry} {}\n", + if splitted.next().is_some() { + "with authToken" + } else { + "without authToken" + } + )); + + child_cmd.env("NPM_CONFIG_REGISTRY", custom_registry); + write_file(job_dir, ".npmrc", content)?; + true + } else { + false + } } } else { false @@ -446,9 +476,11 @@ pub async fn install_bun_lockfile( } } - let mut child_process = start_child_process(child_cmd, &*BUN_PATH, false).await?; + if !has_file { + gen_bunfig(job_dir, job_id, w_id, db).await?; + } - gen_bunfig(job_dir, job_id, w_id, db).await?; + let mut child_process = start_child_process(child_cmd, &*BUN_PATH, false).await?; if let Some(db) = db { handle_child( job_id, diff --git a/backend/windmill-worker/src/deno_executor.rs b/backend/windmill-worker/src/deno_executor.rs index 39b619cae4..66d8ae8673 100644 --- a/backend/windmill-worker/src/deno_executor.rs +++ b/backend/windmill-worker/src/deno_executor.rs @@ -13,7 +13,7 @@ use crate::{ }, get_proxy_envs_for_lang, handle_child::handle_child, - is_sandboxing_enabled, read_ee_registry, DENO_CACHE_DIR, DENO_PATH, HOME_ENV, + is_sandboxing_enabled, read_ee_registry, DENO_CACHE_DIR, DENO_PATH, HOME_ENV, NPMRC, NPM_CONFIG_REGISTRY, PATH_ENV, TZ_ENV, }; use windmill_common::client::AuthedClient; @@ -79,21 +79,29 @@ async fn get_common_deno_proc_envs( ), ]); - let registry = if let Some(conn) = conn { - read_ee_registry( - NPM_CONFIG_REGISTRY.read().await.clone(), - "npm registry", - job_id, - w_id, - conn, - ) - .await + let npmrc = if let Some(conn) = conn { + read_ee_registry(NPMRC.read().await.clone(), "npmrc", job_id, w_id, conn).await } else { - NPM_CONFIG_REGISTRY.read().await.clone() + NPMRC.read().await.clone() }; - if let Some(ref s) = registry { - let (url, _token_opt) = parse_npm_config(s); - deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), url); + + if npmrc.as_ref().map_or(true, |s| s.trim().is_empty()) { + let registry = if let Some(conn) = conn { + read_ee_registry( + NPM_CONFIG_REGISTRY.read().await.clone(), + "npm registry", + job_id, + w_id, + conn, + ) + .await + } else { + NPM_CONFIG_REGISTRY.read().await.clone() + }; + if let Some(ref s) = registry { + let (url, _token_opt) = parse_npm_config(s); + deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), url); + } } if DENO_CERT.len() > 0 { deno_envs.insert(String::from("DENO_CERT"), DENO_CERT.clone()); @@ -390,6 +398,21 @@ try {{ common_deno_proc_envs.insert("HOME".to_string(), job_dir.to_string()); } + let npmrc = read_ee_registry( + NPMRC.read().await.clone(), + "npmrc", + &job.id, + &job.workspace_id, + conn, + ) + .await; + if let Some(ref npmrc_content) = npmrc { + if !npmrc_content.trim().is_empty() { + write_file(job_dir, ".npmrc", npmrc_content)?; + write_file(job_dir, "deno.json", "{}")?; + } + } + //do not cache local dependencies let child = { let reload = format!("--reload={base_internal_url}"); diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 547075933a..71d9f12a90 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -571,6 +571,7 @@ lazy_static::lazy_static! { pub static ref NPM_CONFIG_REGISTRY: Arc>> = Arc::new(RwLock::new(None)); pub static ref BUNFIG_INSTALL_SCOPES: Arc>> = Arc::new(RwLock::new(None)); + pub static ref NPMRC: Arc>> = Arc::new(RwLock::new(None)); pub static ref BUN_NO_CACHE: bool = std::env::var("BUN_NO_CACHE") .ok() .and_then(|x| x.parse::().ok()) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 84a47623bc..55136ec49a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -835,7 +835,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -847,7 +846,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -858,7 +856,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1348,7 +1345,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", "integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1503,7 +1499,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1520,7 +1515,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1537,7 +1531,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1554,7 +1547,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1571,7 +1563,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1588,7 +1579,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1605,7 +1595,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1622,7 +1611,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1639,7 +1627,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1656,7 +1643,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1673,7 +1659,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1690,7 +1675,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1707,7 +1691,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2313,7 +2296,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7193,7 +7175,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7692,7 +7674,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7713,7 +7694,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7734,7 +7714,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7755,7 +7734,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7776,7 +7754,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7797,7 +7774,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7818,7 +7794,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7839,7 +7814,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7860,7 +7834,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7881,7 +7854,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7902,7 +7874,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12529,21 +12500,6 @@ } } }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", diff --git a/frontend/src/lib/components/instanceSettings.ts b/frontend/src/lib/components/instanceSettings.ts index b336b11fca..9e67ff585c 100644 --- a/frontend/src/lib/components/instanceSettings.ts +++ b/frontend/src/lib/components/instanceSettings.ts @@ -406,23 +406,37 @@ export const settings: Record = { ee_only: '' }, { - label: 'Npm config registry', - description: 'Add private npm registry', - key: 'npm_config_registry', - fieldType: 'password', - placeholder: 'https://registry.npmjs.org/:_authToken=npm_FOOBAR', + label: 'NPM Registry Configuration (.npmrc)', + description: + 'Full .npmrc file content for private npm registries. Used by Bun, Deno, and the npm proxy. Takes precedence over the legacy fields below.', + key: 'npmrc', + fieldType: 'codearea', + codeAreaLang: 'ini', + placeholder: + 'registry=https://registry.mycompany.com/\n//registry.mycompany.com/:_authToken=YOUR_TOKEN\n\n@myorg:registry=https://registry.myorg.com/\n//registry.myorg.com/:_authToken=SCOPED_TOKEN', storage: 'setting', ee_only: '' }, { - label: 'Bunfig install scopes', + label: 'Npm config registry (legacy)', + description: 'Add private npm registry. Prefer using the .npmrc field above.', + key: 'npm_config_registry', + fieldType: 'password', + placeholder: 'https://registry.npmjs.org/:_authToken=npm_FOOBAR', + storage: 'setting', + ee_only: '', + hiddenIfEmpty: true + }, + { + label: 'Bunfig install scopes (legacy)', description: - 'Add private scoped registries for Bun, See: https://bun.sh/docs/install/registries', + 'Add private scoped registries for Bun. Prefer using the .npmrc field above. See: https://bun.sh/docs/install/registries', key: 'bunfig_install_scopes', fieldType: 'password', placeholder: '"@myorg3" = { token = "mytoken", url = "https://registry.myorg.com/" }', storage: 'setting', - ee_only: '' + ee_only: '', + hiddenIfEmpty: true }, { label: 'Nuget Config', From b330f388894ecd9cc6b64297420ac6f032d32f72 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 21 Feb 2026 11:37:33 +0000 Subject: [PATCH 03/16] fix: run substitute_ee_code.sh after creating EE worktree Co-Authored-By: Claude Opus 4.6 --- scripts/worktree-env | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/worktree-env b/scripts/worktree-env index 57561e7f77..f1284674f8 100755 --- a/scripts/worktree-env +++ b/scripts/worktree-env @@ -80,4 +80,9 @@ if [ -d "$ee_repo" ]; then elif [ -d "$ee_worktree_dir" ]; then echo "EE worktree already exists at $ee_worktree_dir" fi + + # Create symlinks from backend crates to the EE worktree + if [ -d "$ee_worktree_dir" ] && [ -x "./backend/substitute_ee_code.sh" ]; then + ./backend/substitute_ee_code.sh -d "$ee_worktree_dir" + fi fi From 0d3f956e748aa1496f9eedd4d6dfc12965685264 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 21 Feb 2026 12:06:47 +0000 Subject: [PATCH 04/16] workmux nits --- .workmux.yaml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.workmux.yaml b/.workmux.yaml index 233d509b55..944679a035 100644 --- a/.workmux.yaml +++ b/.workmux.yaml @@ -12,8 +12,24 @@ window_prefix: "wm-" auto_name: model: "claude-sonnet-4.6" - system_prompt: "Generate a kebab-case git branch name." - background: true # Always run in background when using --auto-name + system_prompt: | + Generate a concise git branch name based on the task description. + + Rules: + - Use kebab-case (lowercase with hyphens) + - Keep it short: 1-3 words, max 4 if necessary + - Focus on the core task/feature, not implementation details + - No prefixes like feat/, fix/, chore/ + + Examples of good branch names: + - "Add dark mode toggle" → dark-mode + - "Fix the search results not showing" → fix-search + - "Refactor the authentication module" → auth-refactor + - "Add CSV export to reports" → export-csv + - "Shell completion is broken" → shell-completion + + Output ONLY the branch name, nothing else. + background: true # Commands to run in new worktree before tmux window opens. From fd5ebc2fda589c022074c3bb4dcdb447c7f86cf0 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 21 Feb 2026 21:59:48 +0100 Subject: [PATCH 05/16] fix: tag bunnative dependency jobs as bun instead of nativets (#8045) Co-authored-by: Claude Opus 4.5 --- backend/windmill-queue/src/jobs.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index eaa75f04c1..0c0d885c7b 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -5321,7 +5321,11 @@ async fn push_inner<'c, 'd>( .as_ref() .map(|x| { let tag_lang = if x == &ScriptLang::Bunnative { - ScriptLang::Nativets.as_str() + if job_kind == JobKind::Dependencies { + ScriptLang::Bun.as_str() + } else { + ScriptLang::Nativets.as_str() + } } else { x.as_str() }; From c4de11a406253ff51465987256617f5254a2204e Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 21 Feb 2026 22:04:35 +0100 Subject: [PATCH 06/16] chore(main): release 1.641.0 (#8040) * chore(main): release 1.641.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> --- CHANGELOG.md | 14 ++ backend/Cargo.lock | 140 +++++++++--------- backend/Cargo.toml | 4 +- backend/windmill-api/openapi.yaml | 2 +- benchmarks/lib.ts | 2 +- cli/src/main.ts | 2 +- frontend/package-lock.json | 50 ++++++- frontend/package.json | 2 +- lsp/Pipfile | 4 +- openflow.openapi.yaml | 2 +- .../WindmillClient/WindmillClient.psd1 | 2 +- python-client/wmill/pyproject.toml | 2 +- python-client/wmill_pg/pyproject.toml | 2 +- typescript-client/jsr.json | 2 +- typescript-client/package.json | 2 +- version.txt | 2 +- 16 files changed, 146 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dceb65c0ba..5dff6dec2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [1.641.0](https://github.com/windmill-labs/windmill/compare/v1.640.0...v1.641.0) (2026-02-21) + + +### Features + +* add .npmrc support for private npm registries ([#8039](https://github.com/windmill-labs/windmill/issues/8039)) ([9eb1531](https://github.com/windmill-labs/windmill/commit/9eb15312f663aa6d700e8ac562d7b5c75c2221f7)) + + +### Bug Fixes + +* add created_by ownership check to update/delete saved inputs ([#8038](https://github.com/windmill-labs/windmill/issues/8038)) ([e8a13ed](https://github.com/windmill-labs/windmill/commit/e8a13edde7c0ba2ef80344ab7c7288e7bb2eb6b5)) +* run substitute_ee_code.sh after creating EE worktree ([b330f38](https://github.com/windmill-labs/windmill/commit/b330f388894ecd9cc6b64297420ac6f032d32f72)) +* tag bunnative dependency jobs as bun instead of nativets ([#8045](https://github.com/windmill-labs/windmill/issues/8045)) ([fd5ebc2](https://github.com/windmill-labs/windmill/commit/fd5ebc2fda589c022074c3bb4dcdb447c7f86cf0)) + ## [1.640.0](https://github.com/windmill-labs/windmill/compare/v1.639.0...v1.640.0) (2026-02-20) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index a221bb6fa9..d355cc7f64 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -15725,7 +15725,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-nats", @@ -15789,7 +15789,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -15802,7 +15802,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "argon2", @@ -15940,7 +15940,7 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -15963,7 +15963,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -15976,7 +15976,7 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16002,7 +16002,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.640.0" +version = "1.641.0" dependencies = [ "reqwest 0.12.28", "serde", @@ -16012,7 +16012,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16029,7 +16029,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "base64 0.22.1", @@ -16052,7 +16052,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16075,7 +16075,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16091,7 +16091,7 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16111,7 +16111,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16131,7 +16131,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16145,7 +16145,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-nats", @@ -16171,7 +16171,7 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16196,7 +16196,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "flate2", @@ -16213,7 +16213,7 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16234,7 +16234,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16254,7 +16254,7 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16284,7 +16284,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16311,7 +16311,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.640.0" +version = "1.641.0" dependencies = [ "lazy_static", "serde", @@ -16323,7 +16323,7 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.640.0" +version = "1.641.0" dependencies = [ "argon2", "axum 0.7.9", @@ -16346,7 +16346,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16360,7 +16360,7 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.640.0" +version = "1.641.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16390,7 +16390,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.640.0" +version = "1.641.0" dependencies = [ "chrono", "lazy_static", @@ -16404,7 +16404,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16423,7 +16423,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.640.0" +version = "1.641.0" dependencies = [ "aes-gcm", "anyhow", @@ -16522,7 +16522,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.640.0" +version = "1.641.0" dependencies = [ "chrono", "itertools 0.14.0", @@ -16541,7 +16541,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.640.0" +version = "1.641.0" dependencies = [ "regex", "serde", @@ -16556,7 +16556,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "astral-tokio-tar", @@ -16580,7 +16580,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "futures", @@ -16597,7 +16597,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.640.0" +version = "1.641.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -16613,7 +16613,7 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-trait", @@ -16634,7 +16634,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-trait", @@ -16665,7 +16665,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-oauth2", @@ -16689,7 +16689,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-stream", @@ -16723,7 +16723,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "futures", @@ -16741,7 +16741,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.640.0" +version = "1.641.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -16750,7 +16750,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "lazy_static", @@ -16762,7 +16762,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "serde_json", @@ -16774,7 +16774,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "gosyn", @@ -16786,7 +16786,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "lazy_static", @@ -16798,7 +16798,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "serde_json", @@ -16810,7 +16810,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "nu-parser", @@ -16821,7 +16821,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -16832,7 +16832,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -16845,7 +16845,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-recursion", @@ -16869,7 +16869,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "lazy_static", @@ -16883,7 +16883,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -16900,7 +16900,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "lazy_static", @@ -16915,7 +16915,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "lazy_static", @@ -16934,7 +16934,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "serde", @@ -16945,7 +16945,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-recursion", @@ -16982,7 +16982,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "const_format", @@ -17020,7 +17020,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.640.0" +version = "1.641.0" dependencies = [ "wasm-bindgen", "wasm-bindgen-test", @@ -17030,7 +17030,7 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-recursion", @@ -17059,7 +17059,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -17082,7 +17082,7 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-trait", @@ -17115,7 +17115,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-trait", @@ -17135,7 +17135,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-trait", @@ -17169,7 +17169,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-trait", @@ -17204,7 +17204,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-trait", @@ -17227,7 +17227,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-trait", @@ -17251,7 +17251,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-nats", @@ -17275,7 +17275,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-trait", @@ -17310,7 +17310,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-trait", @@ -17338,7 +17338,7 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-trait", @@ -17361,7 +17361,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "bitflags 2.9.4", @@ -17379,7 +17379,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.640.0" +version = "1.641.0" dependencies = [ "anyhow", "async-once-cell", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 62a7fac618..74163ddf40 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.640.0" +version = "1.641.0" authors.workspace = true edition.workspace = true @@ -76,7 +76,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal"] [workspace.package] -version = "1.640.0" +version = "1.641.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 0b0a2a1cf9..6b11f791b9 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.640.0 + version: 1.641.0 title: Windmill API contact: diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 55e3b9f1b1..77f0603a00 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.640.0"; +export const VERSION = "v1.641.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/main.ts b/cli/src/main.ts index 69c2a28b81..2a1c9f6b93 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -79,7 +79,7 @@ export { // } // }); -export const VERSION = "1.640.0"; +export const VERSION = "1.641.0"; // Re-exported from constants.ts to maintain backwards compatibility export { WM_FORK_PREFIX } from "./core/constants.ts"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 55136ec49a..1c6e10c9e9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.640.0", + "version": "1.641.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.640.0", + "version": "1.641.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -835,6 +835,7 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -846,6 +847,7 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -856,6 +858,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1345,6 +1348,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", "integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1499,6 +1503,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1515,6 +1520,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1531,6 +1537,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1547,6 +1554,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1563,6 +1571,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1579,6 +1588,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1595,6 +1605,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1611,6 +1622,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1627,6 +1639,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1643,6 +1656,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1659,6 +1673,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1675,6 +1690,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1691,6 +1707,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2296,6 +2313,7 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7175,7 +7193,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7674,6 +7692,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7694,6 +7713,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7714,6 +7734,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7734,6 +7755,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7754,6 +7776,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7774,6 +7797,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7794,6 +7818,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7814,6 +7839,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7834,6 +7860,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7854,6 +7881,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7874,6 +7902,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12500,6 +12529,21 @@ } } }, + "node_modules/svelte-check/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index d9a8f856e6..16b0eed47b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.640.0", + "version": "1.641.0", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/lsp/Pipfile b/lsp/Pipfile index 80b01da148..6518a6608b 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,8 +4,8 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.640.0" -wmill_pg = ">=1.640.0" +wmill = ">=1.641.0" +wmill_pg = ">=1.641.0" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 0d90b26c83..3f60492a46 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: '3.0.3' info: - version: 1.640.0 + version: 1.641.0 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index 47328ed62e..392eef58ec 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.640.0' + ModuleVersion = '1.641.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index 3bbce1e7e6..9c223ebe00 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.640.0" +version = "1.641.0" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/python-client/wmill_pg/pyproject.toml b/python-client/wmill_pg/pyproject.toml index 475131067c..67474d7df8 100644 --- a/python-client/wmill_pg/pyproject.toml +++ b/python-client/wmill_pg/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill-pg" -version = "1.640.0" +version = "1.641.0" description = "An extension client for the wmill client library focused on pg" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index 97f8a4a66e..12994d9884 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.640.0", + "version": "1.641.0", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index 80b29804d7..924432dceb 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.640.0", + "version": "1.641.0", "author": "Ruben Fiszel", "license": "Apache 2.0", "sideEffects": false, diff --git a/version.txt b/version.txt index ff49b7ba23..113adc7af4 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.640.0 +1.641.0 From a2cefdf0a22f9c8044fa6b888e8c955cdca0b709 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 21 Feb 2026 22:19:04 +0100 Subject: [PATCH 07/16] refactor(cli): migrate CLI from Deno to Bun/Node.js (#8041) * fix: only enable EE features in test backend when license key is available Co-Authored-By: Claude Opus 4.6 * fix: skip EE tests without license key and exclude test-skills from test discovery Co-Authored-By: Claude Opus 4.6 * fix: unskip passing tests and add duplicate (remote, workspaceId) check in addWorkspace Co-Authored-By: Claude Opus 4.6 * refactor(cli): migrate from Deno APIs to Node.js/Bun-compatible APIs Replace Deno-specific APIs with Node.js equivalents across the entire CLI codebase to enable running on Node.js/Bun. Switch build system from dnt to bun, update imports from jsr:/npm: prefixed to bare specifiers, and add package.json/tsconfig.json for the Node.js ecosystem. Co-Authored-By: Claude Opus 4.6 * all * test(cli): expand test coverage with new integration and unit tests Add standalone_commands.test.ts covering folder list, schedule list, resource-type list/push/update, script show/run/bootstrap, and user commands. Add unit tests for filePathExtensionFromContentType and removeExtensionToPath. Add git_unit, local_encryption_unit, resource_folders_unit, and settings_unit test files. Fix schedule cron expressions (6-field format), add includeSchedules flag, improve test setup with pre-build and auto-cleanup, and support TEST_CLI_RUNTIME=node. Co-Authored-By: Claude Opus 4.6 * fix(cli): replace Deno.readFile with node:fs in WASM loaders and add schema parsing tests Co-Authored-By: Claude Opus 4.5 * refactor(cli): switch WASM parsers from local files to npm packages Use published windmill-parser-wasm-* npm packages instead of local wasm/ files. A loadParser() helper uses createRequire to resolve the .wasm binary from node_modules and passes it to init() via readFileSync, avoiding fetch() and Deno.readFile() patches. Co-Authored-By: Claude Opus 4.6 * test(cli): add coverage for --locks-required lint feature Add 15 tests covering the lock-checking functionality merged from main: - checkMissingLocks: standalone scripts (python, bun, bash), inline lock file resolution (valid, empty, missing), flow inline rawscripts (with/without locks, nested forloopflow), app inline scripts, raw apps without backend folder - runLint --locks-required integration: reports issues when locks missing, skips checks when flag absent, passes when locks exist Co-Authored-By: Claude Opus 4.6 * ci(cli): replace Deno with Bun in CI workflows - cli-tests.yml: remove Deno setup, use `bun test` instead of `deno test`, add `bun install` step for dependency installation - npm_on_release.yml: replace Deno setup with Bun setup for CLI publishing - build.sh: add `bun install` before building so CI has dependencies Co-Authored-By: Claude Opus 4.6 * fix(cli): pre-start backend in test preload and remove Deno test leftovers Co-Authored-By: Claude Opus 4.6 * fix(cli): normalize path separators for Windows compatibility Co-Authored-By: Claude Opus 4.5 * more tests + windows * ci(cli): use Blacksmith runner for Windows tests Switch test-windows job from windows-latest to blacksmith-16vcpu-windows-2025 for faster CI execution. Co-Authored-By: Claude Opus 4.6 * fix(cli): fix Windows path separator expectations in unit tests buildMetadataPath and extractResourceName normalize to forward slashes internally, so tests should not expect platform-specific separators in their output. Co-Authored-By: Claude Opus 4.6 * fix(cli): fix Windows CI test failures for dev_server and script_run Co-Authored-By: Claude Opus 4.6 * fix(cli): set BUN_PATH and NODE_BIN_PATH for backend worker on Windows Co-Authored-By: Claude Opus 4.6 * ci(cli): add SSH debug step on Windows test failure Co-Authored-By: Claude Opus 4.6 * fix(cli): use native path separators for ignore check in dev mode on Windows Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .github/workflows/cli-tests.yml | 43 +- .github/workflows/npm_on_release.yml | 4 +- cli/.npmrc | 1 + cli/build-npm.ts | 83 + cli/build.sh | 11 +- cli/bun.lock | 319 +++ cli/bunfig.toml | 4 + cli/deno.json | 20 - cli/deno.lock | 1806 ----------------- cli/deps.ts | 83 - cli/dnt.ts | 87 - cli/gen_wm_client.sh | 4 +- cli/package.json | 54 + cli/src/commands/app/app.ts | 17 +- cli/src/commands/app/app_metadata.ts | 24 +- cli/src/commands/app/bundle.ts | 12 +- cli/src/commands/app/dev.ts | 247 +-- cli/src/commands/app/generate_agents.ts | 25 +- cli/src/commands/app/lint.ts | 8 +- cli/src/commands/app/new.ts | 61 +- cli/src/commands/app/raw_apps.ts | 43 +- cli/src/commands/dependencies/dependencies.ts | 5 +- cli/src/commands/dev/dev.ts | 78 +- cli/src/commands/flow/flow.ts | 23 +- cli/src/commands/flow/flow_metadata.ts | 25 +- cli/src/commands/folder/folder.ts | 13 +- .../gitsync-settings/gitsync-settings.ts | 2 +- .../gitsync-settings/legacySettings.ts | 9 +- cli/src/commands/gitsync-settings/pull.ts | 15 +- cli/src/commands/gitsync-settings/push.ts | 12 +- cli/src/commands/gitsync-settings/utils.ts | 3 +- cli/src/commands/hub/hub.ts | 4 +- cli/src/commands/init/init.ts | 43 +- cli/src/commands/instance/instance.ts | 78 +- cli/src/commands/jobs/jobs.ts | 6 +- cli/src/commands/lint/lint.ts | 56 +- cli/src/commands/queues/queues.ts | 5 +- .../commands/resource-type/resource-type.ts | 12 +- cli/src/commands/resource/resource.ts | 13 +- cli/src/commands/schedule/schedule.ts | 13 +- cli/src/commands/script/script.ts | 97 +- cli/src/commands/sync/global.ts | 3 +- cli/src/commands/sync/pull.ts | 6 +- cli/src/commands/sync/push.ts | 5 +- cli/src/commands/sync/sync.ts | 129 +- cli/src/commands/trigger/trigger.ts | 12 +- cli/src/commands/user/user.ts | 27 +- cli/src/commands/variable/variable.ts | 14 +- .../commands/worker-groups/worker-groups.ts | 8 +- cli/src/commands/workers/workers.ts | 5 +- cli/src/commands/workspace/fork.ts | 8 +- cli/src/commands/workspace/workspace.ts | 70 +- cli/src/core/auth.ts | 5 +- cli/src/core/branch-profiles.ts | 7 +- cli/src/core/client.ts | 15 + cli/src/core/conf.ts | 16 +- cli/src/core/context.ts | 21 +- cli/src/core/login.ts | 40 +- cli/src/core/settings.ts | 17 +- cli/src/core/specific_items.ts | 2 +- cli/src/core/store.ts | 4 +- cli/src/main.ts | 80 +- cli/src/types.ts | 23 +- cli/src/utils/codebase.ts | 2 +- cli/src/utils/git.ts | 2 +- cli/src/utils/metadata.ts | 161 +- cli/src/utils/resource_folders.ts | 31 +- cli/src/utils/upgrade.ts | 2 +- cli/src/utils/utils.ts | 49 +- cli/src/utils/yaml.ts | 22 + cli/test/cargo_backend.ts | 260 +-- ...ts => cargo_backend_example.standalone.ts} | 72 +- cli/test/conf_branch_override.test.ts | 62 +- cli/test/containerized_backend.ts | 144 +- cli/test/dev_server.test.ts | 417 ++++ .../elements_to_map_branch_specific.test.ts | 99 +- cli/test/folder_schedule_push.test.ts | 409 ++++ cli/test/generate_metadata.test.ts | 230 +++ cli/test/git_unit.test.ts | 73 + cli/test/gitsync_settings_features.test.ts | 63 +- .../include_flags_bypass_filtering.test.ts | 156 +- cli/test/init_no_git_sync.test.ts | 125 +- cli/test/lint_command.test.ts | 234 +-- cli/test/lint_locks.test.ts | 331 +++ cli/test/local_encryption_unit.test.ts | 94 + cli/test/lock_cache.test.ts | 189 +- cli/test/locks_required.test.ts | 620 ------ cli/test/mixed_case_paths.test.ts | 243 +-- cli/test/multi_instance_workspace.test.ts | 97 +- cli/test/override_settings_behavior.test.ts | 92 +- cli/test/preview.test.ts | 415 ++-- cli/test/raw_app_sync.test.ts | 194 +- cli/test/resource_folders_unit.test.ts | 525 +++++ cli/test/script_envs_sync.test.ts | 128 +- cli/test/settings_unit.test.ts | 197 ++ cli/test/setup.ts | 94 + cli/test/specific_items.test.ts | 528 ++--- cli/test/standalone_commands.test.ts | 514 +++++ cli/test/sync_config_resolution.test.ts | 87 +- cli/test/sync_pull_push.test.ts | 1517 ++++++++------ cli/test/test_backend.ts | 44 +- cli/test/test_config_helpers.ts | 13 +- cli/test/utils_unit.test.ts | 567 ++++++ cli/test/variable_resource_push.test.ts | 340 ++++ cli/test/wmill_lock.test.ts | 141 +- cli/test/workspace_conflicts.test.ts | 128 +- cli/test/workspace_deps_filter.test.ts | 222 +- cli/tsconfig.json | 15 + cli/wasm/csharp/windmill_parser_wasm.js | 2 +- cli/wasm/go/windmill_parser_wasm.js | 2 +- cli/wasm/java/windmill_parser_wasm.js | 2 +- cli/wasm/nu/windmill_parser_wasm.js | 2 +- cli/wasm/php/windmill_parser_wasm.js | 2 +- cli/wasm/py/windmill_parser_wasm.js | 2 +- cli/wasm/python/windmill_parser_wasm.js | 2 +- cli/wasm/regex/windmill_parser_wasm.js | 2 +- cli/wasm/ruby/windmill_parser_wasm.js | 2 +- cli/wasm/rust/windmill_parser_wasm.js | 8 +- cli/wasm/ts/windmill_parser_wasm.js | 2 +- cli/wasm/yaml/windmill_parser_wasm.js | 2 +- cli/windmill-utils-internal/remove-ts-ext.sh | 3 +- .../src/config/config.ts | 98 +- 122 files changed, 7738 insertions(+), 6326 deletions(-) create mode 100644 cli/.npmrc create mode 100644 cli/build-npm.ts create mode 100644 cli/bun.lock create mode 100644 cli/bunfig.toml delete mode 100644 cli/deno.json delete mode 100644 cli/deno.lock delete mode 100644 cli/deps.ts delete mode 100644 cli/dnt.ts create mode 100644 cli/package.json create mode 100644 cli/src/core/client.ts create mode 100644 cli/src/utils/yaml.ts rename cli/test/{cargo_backend_example.test.ts => cargo_backend_example.standalone.ts} (55%) create mode 100644 cli/test/dev_server.test.ts create mode 100644 cli/test/folder_schedule_push.test.ts create mode 100644 cli/test/generate_metadata.test.ts create mode 100644 cli/test/git_unit.test.ts create mode 100644 cli/test/lint_locks.test.ts create mode 100644 cli/test/local_encryption_unit.test.ts delete mode 100644 cli/test/locks_required.test.ts create mode 100644 cli/test/resource_folders_unit.test.ts create mode 100644 cli/test/settings_unit.test.ts create mode 100644 cli/test/setup.ts create mode 100644 cli/test/standalone_commands.test.ts create mode 100644 cli/test/utils_unit.test.ts create mode 100644 cli/test/variable_resource_push.test.ts create mode 100644 cli/tsconfig.json diff --git a/.github/workflows/cli-tests.yml b/.github/workflows/cli-tests.yml index c430ae203e..d00646962c 100644 --- a/.github/workflows/cli-tests.yml +++ b/.github/workflows/cli-tests.yml @@ -23,16 +23,16 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Deno - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Generate Windmill client working-directory: cli run: ./gen_wm_client.sh @@ -69,11 +69,6 @@ jobs: cache: true cache-workspaces: backend - - name: Setup Deno - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -90,6 +85,10 @@ jobs: - name: Symlink Node to /usr/bin/node run: sudo ln -sf $(which node) /usr/bin/node + - name: Install dependencies + working-directory: cli + run: bun install + - name: Generate Windmill clients working-directory: cli run: | @@ -101,12 +100,10 @@ jobs: env: DATABASE_URL: postgres://postgres:changeme@localhost:5432 CI_MINIMAL_FEATURES: "true" - run: | - deno test --no-check --allow-all test/ \ - --ignore=test/cargo_backend_example.test.ts + run: bun test --timeout 120000 test/ test-windows: - runs-on: windows-latest + runs-on: blacksmith-16vcpu-windows-2025 steps: - name: Checkout code @@ -126,11 +123,6 @@ jobs: cache: true cache-workspaces: backend - - name: Setup Deno - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -150,6 +142,10 @@ jobs: echo "BUN_PATH=$bunPath" >> $env:GITHUB_OUTPUT echo "NODE_BIN_PATH=$nodePath" >> $env:GITHUB_OUTPUT + - name: Install dependencies + working-directory: cli + run: bun install + - name: Generate Windmill clients working-directory: cli shell: bash @@ -165,9 +161,12 @@ jobs: CI_MINIMAL_FEATURES: "true" BUN_PATH: ${{ steps.runtime-paths.outputs.BUN_PATH }} NODE_BIN_PATH: ${{ steps.runtime-paths.outputs.NODE_BIN_PATH }} - run: | - deno test --no-check --allow-all test/ ` - --ignore=test/cargo_backend_example.test.ts + run: bun test --timeout 120000 test/ + + - name: Keep runner alive for SSH debug + if: failure() + shell: pwsh + run: Start-Sleep -Seconds 3600 # Combined summary job for branch protection test-summary: diff --git a/.github/workflows/npm_on_release.yml b/.github/workflows/npm_on_release.yml index 18c52cb38d..6aa537060e 100644 --- a/.github/workflows/npm_on_release.yml +++ b/.github/workflows/npm_on_release.yml @@ -25,9 +25,9 @@ jobs: with: node-version: "20.x" registry-url: "https://registry.npmjs.org" - - uses: denoland/setup-deno@v2 + - uses: oven-sh/setup-bun@v2 with: - deno-version: v2.x + bun-version: latest - run: cd cli && ./build.sh && cd npm && npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/cli/.npmrc b/cli/.npmrc new file mode 100644 index 0000000000..41583e36ca --- /dev/null +++ b/cli/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/cli/build-npm.ts b/cli/build-npm.ts new file mode 100644 index 0000000000..72be8f1ca9 --- /dev/null +++ b/cli/build-npm.ts @@ -0,0 +1,83 @@ +import { VERSION } from "./src/main.ts"; +import { readFileSync, writeFileSync, rmSync, cpSync } from "node:fs"; +import { join } from "node:path"; + +const outDir = "./npm"; + +// Parser npm packages — used as externals and added to generated package.json +const parserPackages = [ + "windmill-parser-wasm-py", "windmill-parser-wasm-ts", + "windmill-parser-wasm-regex", "windmill-parser-wasm-go", + "windmill-parser-wasm-php", "windmill-parser-wasm-rust", + "windmill-parser-wasm-yaml", "windmill-parser-wasm-csharp", + "windmill-parser-wasm-nu", "windmill-parser-wasm-java", + "windmill-parser-wasm-ruby", +]; +const parserExternals = parserPackages.flatMap(p => ["--external", p]); + +// Clean output directory +rmSync(outDir, { recursive: true, force: true }); + +// Build with bun — bundle everything except esbuild (platform-specific binary), +// svelte (optional, only needed for `wmill app bundle/dev`), and parser packages +// (loaded at runtime via init() with readFileSync for the .wasm binary). +console.log("Bundling with bun build..."); +const buildResult = Bun.spawnSync([ + "bun", "build", "src/main.ts", + "--outdir", join(outDir, "esm"), + "--target", "node", + "--format", "esm", + "--external", "esbuild", + "--external", "svelte", + "--external", "svelte/compiler", + ...parserExternals, +], { cwd: import.meta.dir, stdout: "inherit", stderr: "inherit" }); + +if (buildResult.exitCode !== 0) { + console.error("Build failed"); + process.exit(1); +} + +// Add shebang to main.js +const mainJsPath = join(outDir, "esm", "main.js"); +const mainJs = readFileSync(mainJsPath, "utf-8"); +writeFileSync(mainJsPath, "#!/usr/bin/env node\n" + mainJs, "utf-8"); + +// Copy LICENSE and README +cpSync("../LICENSE", join(outDir, "LICENSE")); +cpSync("README.md", join(outDir, "README.md")); + +// Generate package.json +const packageJson = { + name: "windmill-cli", + version: VERSION, + description: "CLI for Windmill", + license: "Apache 2.0", + type: "module", + main: "esm/main.js", + bin: { + wmill: "esm/main.js", + }, + repository: { + type: "git", + url: "git+https://github.com/windmill-labs/windmill.git", + }, + bugs: { + url: "https://github.com/windmill-labs/windmill/issues", + }, + dependencies: { + esbuild: "^0.24.2", + ...Object.fromEntries(parserPackages.map(p => [p, "*"])), + }, + optionalDependencies: { + svelte: "^5.0.0", + }, +}; + +writeFileSync( + join(outDir, "package.json"), + JSON.stringify(packageJson, null, 2) + "\n", + "utf-8" +); + +console.log(`Built npm package v${VERSION} to ${outDir}/`); diff --git a/cli/build.sh b/cli/build.sh index 9a86ccbb54..62943ce042 100755 --- a/cli/build.sh +++ b/cli/build.sh @@ -9,12 +9,11 @@ set -e # Generate utils client files ./windmill-utils-internal/gen_wm_client.sh -# Add .ts extensions to windmill-utils-internal -./windmill-utils-internal/remove-ts-ext.sh -r +# Install dependencies +bun install -# Run dnt -echo "Running dnt..." -deno run -A dnt.ts +# Build npm package with bun +echo "Building npm package..." +bun run build-npm.ts echo "Build complete!" - diff --git a/cli/bun.lock b/cli/bun.lock new file mode 100644 index 0000000000..951ba47c7c --- /dev/null +++ b/cli/bun.lock @@ -0,0 +1,319 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "windmill-cli-dev", + "dependencies": { + "@ayonli/jsext": "^1.9.0", + "@cliffy/ansi": "npm:@jsr/cliffy__ansi@1.0.0", + "@cliffy/command": "npm:@jsr/cliffy__command@1.0.0", + "@cliffy/prompt": "npm:@jsr/cliffy__prompt@1.0.0", + "@cliffy/table": "npm:@jsr/cliffy__table@1.0.0", + "@std/encoding": "npm:@jsr/std__encoding@1.0.10", + "@std/log": "npm:@jsr/std__log@0.224.14", + "@std/path": "npm:@jsr/std__path@1.1.4", + "@std/yaml": "npm:@jsr/std__yaml@1.0.10", + "@windmill-labs/shared-utils": "npm:@jsr/windmill-labs__shared-utils@1.0.12", + "diff": "^5.2.0", + "esbuild": "0.24.2", + "get-port": "7.1.0", + "jszip": "3.8.0", + "minimatch": "^10.0.0", + "open": "^10.0.0", + "svelte": "^5.45.2", + "windmill-parser-wasm-csharp": "*", + "windmill-parser-wasm-go": "*", + "windmill-parser-wasm-java": "*", + "windmill-parser-wasm-nu": "*", + "windmill-parser-wasm-php": "*", + "windmill-parser-wasm-py": "*", + "windmill-parser-wasm-regex": "*", + "windmill-parser-wasm-ruby": "*", + "windmill-parser-wasm-rust": "*", + "windmill-parser-wasm-ts": "*", + "windmill-parser-wasm-yaml": "*", + "windmill-yaml-validator": "1.1.1", + "ws": "8.18.0", + "yaml": "^2.7.0", + }, + "devDependencies": { + "@types/diff": "^5.2.3", + "@types/node": "^22.0.0", + "@types/ws": "^8.5.0", + "typescript": "^5.7.0", + }, + }, + }, + "packages": { + "@ayonli/jsext": ["@ayonli/jsext@1.9.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "sudo-prompt": "^9.2.1", "ws": "^8.17.0", "zod": "^3.23.8" } }, "sha512-hIu6lQhoLr5e26lmt+vzopuZffaAyb623r4+8HlN/rhXgm2ywHslzk7UHiATdfDbfPjBARkB6cfXjVEi3aav6g=="], + + "@cliffy/ansi": ["@jsr/cliffy__ansi@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__ansi/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__internal": "1.0.0", "@jsr/std__encoding": "^1.0.10", "@jsr/std__fmt": "^1.0.9", "@jsr/std__io": "~0.225.3" } }, "sha512-JesgTdgR0aW1mZv96VqvRHr2efzr4MgDFMnoT+hkhaiCpmyBz33sHM5peAoMJUbGVfEfQAsysIXvvgoFYoveYg=="], + + "@cliffy/command": ["@jsr/cliffy__command@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__command/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__flags": "1.0.0", "@jsr/cliffy__internal": "1.0.0", "@jsr/cliffy__table": "1.0.0", "@jsr/std__fmt": "^1.0.9", "@jsr/std__semver": "^1.0.8", "@jsr/std__text": "^1.0.17" } }, "sha512-oObplVtu1tvpkhgpuPDHZidx9g3axVOfRMQGmw7ZSGxp0+vZIJGiEtpcSvlN0XfuEhOG8neqfVBSSE9txrKanw=="], + + "@cliffy/prompt": ["@jsr/cliffy__prompt@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__prompt/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__ansi": "1.0.0", "@jsr/cliffy__internal": "1.0.0", "@jsr/cliffy__keycode": "1.0.0", "@jsr/std__assert": "^1.0.18", "@jsr/std__fmt": "^1.0.9", "@jsr/std__io": "~0.225.3", "@jsr/std__path": "^1.1.4", "@jsr/std__text": "^1.0.17" } }, "sha512-JDuHcCAjScV0IUj389brneF6AzJyyP0pK8mymsrGN5/PGQfqK8zr96QpFlo1wmo8BY/3JQAdNfy6NZkPCJ6VWA=="], + + "@cliffy/table": ["@jsr/cliffy__table@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__table/1.0.0.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.9" } }, "sha512-VoLxH0DjofHWPWKUc5N+oCwXB6O6e+carnhp23yJTa7qokBb+SCrTIABEgQdIe/p0bxgmZhz17xt2efaAxXvbQ=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.24.2", "", { "os": "android", "cpu": "x64" }, "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.2", "", { "os": "none", "cpu": "x64" }, "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@jsr/cliffy__ansi": ["@jsr/cliffy__ansi@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__ansi/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__internal": "1.0.0", "@jsr/std__encoding": "^1.0.10", "@jsr/std__fmt": "^1.0.9", "@jsr/std__io": "~0.225.3" } }, "sha512-JesgTdgR0aW1mZv96VqvRHr2efzr4MgDFMnoT+hkhaiCpmyBz33sHM5peAoMJUbGVfEfQAsysIXvvgoFYoveYg=="], + + "@jsr/cliffy__flags": ["@jsr/cliffy__flags@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__flags/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__internal": "1.0.0", "@jsr/std__text": "^1.0.17" } }, "sha512-j/v3J8MWu0tkYyisZ2w1HxELxxL/qg6vey9+fRkbTJ+S9J0GeLUn2joouikG7aXpULKCXHTjJ9XH9gQx+F3npw=="], + + "@jsr/cliffy__internal": ["@jsr/cliffy__internal@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__internal/1.0.0.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.9" } }, "sha512-YPkbccbuu+kE55k+nia5jJx5Tu/IolBDXZTAgEA+YRGOzq8I1VkXajwykFXvSbXeVee3zQBU7y0HajVDB7ujQA=="], + + "@jsr/cliffy__keycode": ["@jsr/cliffy__keycode@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__keycode/1.0.0.tgz", {}, "sha512-1ot+y8oZheBTpfgCazWjSOAK2Y2nOQD7NwMuiSAkcRuc1t7VizQZfDpZtBx97NlkYWgjn6ylArt2xyhiyLKRhA=="], + + "@jsr/cliffy__table": ["@jsr/cliffy__table@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__table/1.0.0.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.9" } }, "sha512-VoLxH0DjofHWPWKUc5N+oCwXB6O6e+carnhp23yJTa7qokBb+SCrTIABEgQdIe/p0bxgmZhz17xt2efaAxXvbQ=="], + + "@jsr/std__assert": ["@jsr/std__assert@1.0.19", "https://npm.jsr.io/~/11/@jsr/std__assert/1.0.19.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12" } }, "sha512-pEj6RPkGbqlgRmyKwATp4cUs6+ijxtdrv3bq8v1d2I2CEcMEyPaO8cVKro61wGRDH4cNg8Zx6haztvK/9m7gkA=="], + + "@jsr/std__bytes": ["@jsr/std__bytes@1.0.6", "https://npm.jsr.io/~/11/@jsr/std__bytes/1.0.6.tgz", {}, "sha512-St6yKggjFGhxS52IFLJWvkchRFbAKg2Xh8UxA4S1EGz7GJ2Ui+ssDDldj/w2c8vCxvl6qgR0HaYbKeFJNqujmA=="], + + "@jsr/std__encoding": ["@jsr/std__encoding@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz", {}, "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw=="], + + "@jsr/std__fmt": ["@jsr/std__fmt@1.0.9", "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.9.tgz", {}, "sha512-YFJJMozmORj2K91c5J9opWeh0VUwrd+Mwb7Pr0FkVCAKVLu2UhT4LyvJqWiyUT+eF+MdfqQ9F7RtQj4bXn9Smw=="], + + "@jsr/std__fs": ["@jsr/std__fs@1.0.21", "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.21.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12", "@jsr/std__path": "^1.1.4" } }, "sha512-k/agrcKGm6KD89ci3AEyRmu3wRWf9JZNliOF4ZUxagTHiySmxjiKU3Lk+d2ksRtwEi7oWlLGS0AVM9Lciwc/xg=="], + + "@jsr/std__internal": ["@jsr/std__internal@1.0.12", "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz", {}, "sha512-6xReMW9p+paJgqoFRpOE2nogJFvzPfaLHLIlyADYjKMUcwDyjKZxryIbgcU+gxiTygn8yCjld1HoI0ET4/iZeA=="], + + "@jsr/std__io": ["@jsr/std__io@0.225.3", "https://npm.jsr.io/~/11/@jsr/std__io/0.225.3.tgz", { "dependencies": { "@jsr/std__bytes": "^1.0.6" } }, "sha512-IDXY253ipW6FV34CJVxO+3ubfvSEEzw9N2W303KnLe9K/Y9+v/ID1dQYf9VsCCOFMpFtCmOLqzIZsRqv6yQnWw=="], + + "@jsr/std__path": ["@jsr/std__path@1.1.4", "https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12" } }, "sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA=="], + + "@jsr/std__regexp": ["@jsr/std__regexp@1.0.1", "https://npm.jsr.io/~/11/@jsr/std__regexp/1.0.1.tgz", {}, "sha512-AnGeP//DHpPvhCWjI5dR4o013JhCQioD8yMF8drD7PWb0X4kvmO35hbZi+NZhfSolz4Ts2cpPzJY+DUpi2XE9A=="], + + "@jsr/std__semver": ["@jsr/std__semver@1.0.8", "https://npm.jsr.io/~/11/@jsr/std__semver/1.0.8.tgz", {}, "sha512-YhkykPU2Majz66e+rQbP0okYc7kKv+U32aguLPCXZZAL+vEVmBA+khHjPHhLBpWR073gzU3WHqGRgB7a/aXCjg=="], + + "@jsr/std__text": ["@jsr/std__text@1.0.17", "https://npm.jsr.io/~/11/@jsr/std__text/1.0.17.tgz", { "dependencies": { "@jsr/std__regexp": "^1.0.1" } }, "sha512-oZsihl1bcTy1Ixzven8rin8kjChj1zDJWqgpS0oSMGCJDzyB365gtIfAvcMmji+M+FcIWo3goDXfHcFYt+k/kg=="], + + "@std/encoding": ["@jsr/std__encoding@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz", {}, "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw=="], + + "@std/log": ["@jsr/std__log@0.224.14", "https://npm.jsr.io/~/11/@jsr/std__log/0.224.14.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.5", "@jsr/std__fs": "^1.0.11", "@jsr/std__io": "^0.225.2" } }, "sha512-EHT7E0plakyzk/gxMrwqUf3YGCCxN3Is25QrEh7toYA7qwj46R4qY7cIaDEKy8QqI5JHOFHwWXOClcPK6goIoQ=="], + + "@std/path": ["@jsr/std__path@1.1.4", "https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12" } }, "sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA=="], + + "@std/yaml": ["@jsr/std__yaml@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__yaml/1.0.10.tgz", {}, "sha512-1WIM023Kvi48pvPE3UO5YcieambLgywUooLhAkkaObIcMB77F/YP2ILdl+vNfik+vElkl9znmuST9AZo8mbCpA=="], + + "@stoplight/ordered-object-literal": ["@stoplight/ordered-object-literal@1.0.5", "", {}, "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg=="], + + "@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], + + "@stoplight/yaml": ["@stoplight/yaml@4.3.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.5", "@stoplight/types": "^14.1.1", "@stoplight/yaml-ast-parser": "0.0.50", "tslib": "^2.2.0" } }, "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w=="], + + "@stoplight/yaml-ast-parser": ["@stoplight/yaml-ast-parser@0.0.50", "", {}, "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="], + + "@types/diff": ["@types/diff@5.2.3", "", {}, "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@windmill-labs/shared-utils": ["@jsr/windmill-labs__shared-utils@1.0.12", "https://npm.jsr.io/~/11/@jsr/windmill-labs__shared-utils/1.0.12.tgz", {}, "sha512-bJOacyfxxNPwNTzA4AxCB5iGFop0h3mCgs+E9j3ZaJYDo1soblY16CebnQ56EPy/M3V344X/QoOFBORyRo1Mnw=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + + "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "devalue": ["devalue@5.6.3", "", {}, "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg=="], + + "diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], + + "esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "get-port": ["get-port@7.1.0", "", {}, "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "jszip": ["jszip@3.8.0", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "set-immediate-shim": "~1.0.1" } }, "sha512-cnpQrXvFSLdsR9KR5/x7zdf6c3m8IhZfZzSblFEHSqBaVwD2nvJ4CuCKLyvKvwBgZm08CgfSoiTBQLm5WW9hGw=="], + + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], + + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "set-immediate-shim": ["set-immediate-shim@1.0.1", "", {}, "sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ=="], + + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "sudo-prompt": ["sudo-prompt@9.2.1", "", {}, "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw=="], + + "svelte": ["svelte@5.53.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-WzxFHZhhD23Qzu7JCYdvm1rxvRSzdt9HtHO8TScMBX51bLRFTcJmATVqjqXG+6Ln6hrViGCo9DzwOhAasxwC/w=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], + + "windmill-parser-wasm-csharp": ["windmill-parser-wasm-csharp@1.510.1", "", {}, "sha512-qm09YmnbeYHLwYn1jUnObVzPhYO9NZKMlIO7nlo7zPJBXqksgG5fK/KCtwGw9rChrnz+DsvM9wP5FhrwRLMtwQ=="], + + "windmill-parser-wasm-go": ["windmill-parser-wasm-go@1.510.1", "", {}, "sha512-HOkk6LXK0wrwvkn+zjm3Gxo90HmyL6TYqmLo2yp8fZuppy7GOngT27zwYeBtwONiPyvDKskzoqPQoEfd8VuUsQ=="], + + "windmill-parser-wasm-java": ["windmill-parser-wasm-java@1.510.1", "", {}, "sha512-Zle+JZT/ZwUArUVacUudYlS+CaHp2lSnkqD/IhWaRUG+gcv26VbERnrrHPonqXbVMS+eA9ElfXrFM5j0ukaXUw=="], + + "windmill-parser-wasm-nu": ["windmill-parser-wasm-nu@1.510.1", "", {}, "sha512-AJLFiUy6af+LpUe7CddDo4+JOmw3c0K/1iOWh8NdTwXcLDj90lL6089mdsVo1apyloLgrTbcuFDzZMXVGBgtCg=="], + + "windmill-parser-wasm-php": ["windmill-parser-wasm-php@1.574.1", "", {}, "sha512-COyid6B1RYs+bpzUCInsA4HY/WZkpDLfkQ90+AqU/TVTpzYSbAC2JCbIwy0cRElBvlhI4bQ+9Wg6hSQKMpEkpA=="], + + "windmill-parser-wasm-py": ["windmill-parser-wasm-py@1.628.3", "", {}, "sha512-TlluqknZpg8cZ+A3m6JFLPseY2PpKtDsxdj26fAnCUzKPtse8TxQR+n0dwC80rfW5TwdWSulvNGRDgcNuf7CTw=="], + + "windmill-parser-wasm-regex": ["windmill-parser-wasm-regex@1.639.0", "", {}, "sha512-qvYM4sYxB6M0xrqwBljS2fWqOMk6rp++60TRltJnzZDzVaWQrKjTGwNMmfepGAIWy1OGVKp0SCVERhe2P+O6tQ=="], + + "windmill-parser-wasm-ruby": ["windmill-parser-wasm-ruby@1.526.1", "", {}, "sha512-rMBQA8s21wmL2kA5ztRs/ZgVA3ckxe9/NLjxl3iQPL0CX6DlvfaUH0O+AnhpXXDMyBs1Y1SZIhcnbnvsHZ3R8g=="], + + "windmill-parser-wasm-rust": ["windmill-parser-wasm-rust@1.558.1", "", {}, "sha512-21S7lm1KF8zO1187rbq14hzPHII2RdM2+D44MoAh1F6VoaScj+Puq0z5B1O/hwn/95R/a9jBlL2D8jbkXtlD1A=="], + + "windmill-parser-wasm-ts": ["windmill-parser-wasm-ts@1.623.1", "", {}, "sha512-FBwi/zXxjhZcCvi04oFdNivazru1ynIqSbafHSArfaaBWesBO3nye9UO/WXUlWZm5a7BExbU+3R/eVJrGaornw=="], + + "windmill-parser-wasm-yaml": ["windmill-parser-wasm-yaml@1.593.0", "", {}, "sha512-Gyx4aR2jsJYuDrD3mCNTmz7LWOQQXPw5yKNCC1xRgUOPfjsD/tINAFfsBLwVOSmlQQcFZO+wHm4KtDtXOcnGVw=="], + + "windmill-yaml-validator": ["windmill-yaml-validator@1.1.1", "", { "dependencies": { "@stoplight/yaml": "^4.3.0", "ajv": "^8.17.1" } }, "sha512-CVgAwEoBdJhF39q2N012QffhlGPRIyIWd8gj7NnfG+/lMWgH2k5CBLtKIt6cPF8Bxz+6DGC3st1ARSsecDtbTg=="], + + "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + } +} diff --git a/cli/bunfig.toml b/cli/bunfig.toml new file mode 100644 index 0000000000..7fe1604012 --- /dev/null +++ b/cli/bunfig.toml @@ -0,0 +1,4 @@ +[test] +preload = ["./test/setup.ts"] +timeout = 60000 +root = "./test" diff --git a/cli/deno.json b/cli/deno.json deleted file mode 100644 index 0c14793da7..0000000000 --- a/cli/deno.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "imports": { - "@cliffy/ansi": "jsr:@windmill-labs/cliffy-ansi@^1.0.0-rc.5", - "@cliffy/command": "jsr:@windmill-labs/cliffy-command@^1.0.0-rc.5", - "@cliffy/prompt": "jsr:@windmill-labs/cliffy-prompt@^1.0.0-rc.6", - "@cliffy/table": "jsr:@windmill-labs/cliffy-table@^1.0.0-rc.5", - "@deno/dnt": "jsr:@deno/dnt@^0.41.3", - "@std/encoding": "jsr:@std/encoding@^1.0.10", - "@std/fs": "jsr:@std/fs@^1.0.21", - "@std/io": "jsr:@std/io@^0.224.9", - "@std/log": "jsr:@std/log@^0.224.14", - "@std/net": "jsr:@std/net@^1.0.6", - "@std/path": "jsr:@std/path@^1.1.4", - "@std/streams": "jsr:@std/streams@^1.0.16", - "@std/yaml": "jsr:@std/yaml@^1.0.10", - "@types/diff": "npm:@types/diff@^5.2.3", - "ws": "npm:ws@8.18.0" - }, - "nodeModulesDir": "auto" -} \ No newline at end of file diff --git a/cli/deno.lock b/cli/deno.lock deleted file mode 100644 index 03eca92bfd..0000000000 --- a/cli/deno.lock +++ /dev/null @@ -1,1806 +0,0 @@ -{ - "version": "5", - "specifiers": { - "jsr:@david/code-block-writer@^13.0.2": "13.0.3", - "jsr:@david/code-block-writer@^13.0.3": "13.0.3", - "jsr:@deno/cache-dir@~0.10.3": "0.10.3", - "jsr:@deno/dnt@0.41.3": "0.41.3", - "jsr:@deno/dnt@0.42.3": "0.42.3", - "jsr:@deno/dnt@~0.41.3": "0.41.3", - "jsr:@deno/graph@~0.73.1": "0.73.1", - "jsr:@std/assert@0.223": "0.223.0", - "jsr:@std/assert@0.226": "0.226.0", - "jsr:@std/assert@1.0.0-rc.2": "1.0.0-rc.2", - "jsr:@std/bytes@0.223": "0.223.0", - "jsr:@std/bytes@^1.0.2": "1.0.6", - "jsr:@std/bytes@^1.0.5": "1.0.6", - "jsr:@std/bytes@^1.0.6": "1.0.6", - "jsr:@std/cli@1.0.0-rc.2": "1.0.0-rc.2", - "jsr:@std/encoding@1.0.0-rc.2": "1.0.0-rc.2", - "jsr:@std/encoding@1.0.4": "1.0.4", - "jsr:@std/encoding@^1.0.10": "1.0.10", - "jsr:@std/fmt@0.223": "0.223.0", - "jsr:@std/fmt@1": "1.0.8", - "jsr:@std/fmt@^1.0.5": "1.0.8", - "jsr:@std/fmt@~0.225.4": "0.225.6", - "jsr:@std/fs@*": "1.0.22", - "jsr:@std/fs@0.223": "0.223.0", - "jsr:@std/fs@1": "1.0.22", - "jsr:@std/fs@^1.0.11": "1.0.22", - "jsr:@std/fs@^1.0.21": "1.0.22", - "jsr:@std/fs@~0.229.3": "0.229.3", - "jsr:@std/internal@^1.0.12": "1.0.12", - "jsr:@std/io@*": "0.225.2", - "jsr:@std/io@0.223": "0.223.0", - "jsr:@std/io@~0.224.2": "0.224.9", - "jsr:@std/io@~0.224.9": "0.224.9", - "jsr:@std/io@~0.225.2": "0.225.2", - "jsr:@std/log@*": "0.224.14", - "jsr:@std/log@~0.224.14": "0.224.14", - "jsr:@std/net@^1.0.6": "1.0.6", - "jsr:@std/path@*": "1.1.4", - "jsr:@std/path@0.223": "0.223.0", - "jsr:@std/path@1": "1.1.4", - "jsr:@std/path@1.0.0-rc.1": "1.0.0-rc.1", - "jsr:@std/path@1.0.0-rc.2": "1.0.0-rc.2", - "jsr:@std/path@^1.1.3": "1.1.4", - "jsr:@std/path@^1.1.4": "1.1.4", - "jsr:@std/path@~0.225.2": "0.225.2", - "jsr:@std/streams@^1.0.16": "1.0.17", - "jsr:@std/text@1.0.0-rc.1": "1.0.0-rc.1", - "jsr:@std/yaml@*": "1.0.10", - "jsr:@std/yaml@^1.0.10": "1.0.10", - "jsr:@ts-morph/bootstrap@0.24": "0.24.0", - "jsr:@ts-morph/bootstrap@0.27": "0.27.0", - "jsr:@ts-morph/common@0.24": "0.24.0", - "jsr:@ts-morph/common@0.27": "0.27.0", - "jsr:@windmill-labs/cliffy-ansi@1.0.0-rc.5": "1.0.0-rc.5", - "jsr:@windmill-labs/cliffy-ansi@^1.0.0-rc.5": "1.0.0-rc.5", - "jsr:@windmill-labs/cliffy-command@1.0.0-rc.5": "1.0.0-rc.5", - "jsr:@windmill-labs/cliffy-command@^1.0.0-rc.5": "1.0.0-rc.5", - "jsr:@windmill-labs/cliffy-flags@1.0.0-rc.5": "1.0.0-rc.5", - "jsr:@windmill-labs/cliffy-internal@1.0.0-rc.5": "1.0.0-rc.5", - "jsr:@windmill-labs/cliffy-keycode@1.0.0-rc.5": "1.0.0-rc.5", - "jsr:@windmill-labs/cliffy-prompt@1.0.0-rc.6": "1.0.0-rc.6", - "jsr:@windmill-labs/cliffy-prompt@^1.0.0-rc.6": "1.0.0-rc.6", - "jsr:@windmill-labs/cliffy-table@1.0.0-rc.5": "1.0.0-rc.5", - "jsr:@windmill-labs/cliffy-table@^1.0.0-rc.5": "1.0.0-rc.5", - "jsr:@windmill-labs/shared-utils@1.0.10": "1.0.10", - "jsr:@windmill-labs/shared-utils@1.0.11": "1.0.11", - "jsr:@windmill-labs/shared-utils@1.0.12": "1.0.12", - "jsr:@windmill-labs/shared-utils@1.0.3": "1.0.3", - "jsr:@windmill-labs/shared-utils@1.0.5": "1.0.5", - "jsr:@windmill-labs/shared-utils@1.0.6": "1.0.6", - "jsr:@windmill-labs/shared-utils@1.0.7": "1.0.7", - "jsr:@windmill-labs/shared-utils@^1.0.10": "1.0.12", - "jsr:@windmill-labs/shared-utils@^1.0.12": "1.0.12", - "jsr:@windmill-labs/shared-utils@^1.0.8": "1.0.12", - "jsr:@windmill-labs/shared-utils@^1.0.9": "1.0.12", - "npm:@ayonli/jsext@*": "1.8.0", - "npm:@types/diff@^5.2.3": "5.2.3", - "npm:@types/node@*": "24.2.0", - "npm:@types/ws@*": "8.18.1", - "npm:@windmill-labs/shared-utils@1.0.1": "1.0.1", - "npm:@windmill-labs/shared-utils@1.0.2": "1.0.2", - "npm:centdix-utils@*": "1.0.15", - "npm:diff@*": "8.0.2", - "npm:es-main@*": "1.3.0", - "npm:esbuild-plugin-vue3@0.5.1": "0.5.1_vue@3.5.25__typescript@4.9.5_typescript@4.9.5", - "npm:esbuild-svelte@0.9.3": "0.9.3_esbuild@0.24.2_svelte@5.45.2__acorn@8.14.1", - "npm:esbuild@*": "0.24.2", - "npm:esbuild@0.24.2": "0.24.2", - "npm:express@*": "5.1.0", - "npm:get-port@7.1.0": "7.1.0", - "npm:jszip@3.7.1": "3.7.1", - "npm:jszip@3.8.0": "3.8.0", - "npm:minimatch@*": "10.0.3", - "npm:open@*": "10.2.0", - "npm:svelte-preprocess@6.0.3": "6.0.3_svelte@5.45.2__acorn@8.14.1", - "npm:svelte@5.45.2": "5.45.2_acorn@8.14.1", - "npm:windmill-yaml-validator@1.1.0": "1.1.0", - "npm:windmill-yaml-validator@1.1.1": "1.1.1", - "npm:ws@*": "8.18.3", - "npm:ws@8.18.0": "8.18.0", - "npm:ws@8.18.3": "8.18.3" - }, - "jsr": { - "@david/code-block-writer@13.0.2": { - "integrity": "14dd3baaafa3a2dea8bf7dfbcddeccaa13e583da2d21d666c01dc6d681cd74ad" - }, - "@david/code-block-writer@13.0.3": { - "integrity": "f98c77d320f5957899a61bfb7a9bead7c6d83ad1515daee92dbacc861e13bb7f" - }, - "@deno/cache-dir@0.10.3": { - "integrity": "eb022f84ecc49c91d9d98131c6e6b118ff63a29e343624d058646b9d50404776", - "dependencies": [ - "jsr:@deno/graph", - "jsr:@std/fmt@0.223", - "jsr:@std/fs@0.223", - "jsr:@std/io@0.223", - "jsr:@std/path@0.223" - ] - }, - "@deno/dnt@0.41.3": { - "integrity": "b2ef2c8a5111eef86cb5bfcae103d6a2938e8e649e2461634a7befb7fc59d6d2", - "dependencies": [ - "jsr:@david/code-block-writer@^13.0.2", - "jsr:@deno/cache-dir", - "jsr:@std/fmt@1", - "jsr:@std/fs@1", - "jsr:@std/path@1", - "jsr:@ts-morph/bootstrap@0.24" - ] - }, - "@deno/dnt@0.42.3": { - "integrity": "62a917a0492f3c8af002dce90605bb0d41f7d29debc06aca40dba72ab65d8ae3", - "dependencies": [ - "jsr:@david/code-block-writer@^13.0.3", - "jsr:@std/fmt@1", - "jsr:@std/fs@1", - "jsr:@std/path@1", - "jsr:@ts-morph/bootstrap@0.27" - ] - }, - "@deno/graph@0.73.1": { - "integrity": "cd69639d2709d479037d5ce191a422eabe8d71bb68b0098344f6b07411c84d41" - }, - "@std/assert@0.223.0": { - "integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24" - }, - "@std/assert@0.226.0": { - "integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3" - }, - "@std/assert@1.0.0-rc.2": { - "integrity": "0484eab1d76b55fca1c3beaff485a274e67dd3b9f065edcbe70030dfc0b964d3" - }, - "@std/bytes@0.223.0": { - "integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8" - }, - "@std/bytes@1.0.6": { - "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" - }, - "@std/cli@1.0.0-rc.2": { - "integrity": "97dfae82b9f0e189768ebfa7a5da53375955b94bad0a1804f8e3b73563b03787" - }, - "@std/encoding@1.0.0-rc.2": { - "integrity": "160d7674a20ebfbccdf610b3801fee91cf6e42d1c106dd46bbaf46e395cd35ef" - }, - "@std/encoding@1.0.4": { - "integrity": "2266cd516b32369e3dc5695717c96bf88343a1f761d6e6187a02a2bbe2af86ae" - }, - "@std/encoding@1.0.10": { - "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" - }, - "@std/fmt@0.223.0": { - "integrity": "6deb37794127dfc7d7bded2586b9fc6f5d50e62a8134846608baf71ffc1a5208" - }, - "@std/fmt@0.225.6": { - "integrity": "aba6aea27f66813cecfd9484e074a9e9845782ab0685c030e453a8a70b37afc8" - }, - "@std/fmt@1.0.8": { - "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" - }, - "@std/fs@0.223.0": { - "integrity": "3b4b0550b2c524cbaaa5a9170c90e96cbb7354e837ad1bdaf15fc9df1ae9c31c" - }, - "@std/fs@0.229.3": { - "integrity": "783bca21f24da92e04c3893c9e79653227ab016c48e96b3078377ebd5222e6eb", - "dependencies": [ - "jsr:@std/path@1.0.0-rc.1" - ] - }, - "@std/fs@1.0.20": { - "integrity": "e953206aae48d46ee65e8783ded459f23bec7dd1f3879512911c35e5484ea187", - "dependencies": [ - "jsr:@std/internal", - "jsr:@std/path@^1.1.3" - ] - }, - "@std/fs@1.0.22": { - "integrity": "de0f277a58a867147a8a01bc1b181d0dfa80bfddba8c9cf2bacd6747bcec9308", - "dependencies": [ - "jsr:@std/internal", - "jsr:@std/path@^1.1.4" - ] - }, - "@std/internal@1.0.12": { - "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" - }, - "@std/io@0.223.0": { - "integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1", - "dependencies": [ - "jsr:@std/assert@0.223", - "jsr:@std/bytes@0.223" - ] - }, - "@std/io@0.224.9": { - "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3", - "dependencies": [ - "jsr:@std/bytes@^1.0.2" - ] - }, - "@std/io@0.225.2": { - "integrity": "3c740cd4ee4c082e6cfc86458f47e2ab7cb353dc6234d5e9b1f91a2de5f4d6c7", - "dependencies": [ - "jsr:@std/bytes@^1.0.5" - ] - }, - "@std/log@0.224.14": { - "integrity": "257f7adceee3b53bb2bc86c7242e7d1bc59729e57d4981c4a7e5b876c808f05e", - "dependencies": [ - "jsr:@std/fmt@^1.0.5", - "jsr:@std/fs@^1.0.11", - "jsr:@std/io@~0.225.2" - ] - }, - "@std/net@1.0.6": { - "integrity": "110735f93e95bb9feb95790a8b1d1bf69ec0dc74f3f97a00a76ea5efea25500c" - }, - "@std/path@0.223.0": { - "integrity": "593963402d7e6597f5a6e620931661053572c982fc014000459edc1f93cc3989", - "dependencies": [ - "jsr:@std/assert@0.223" - ] - }, - "@std/path@0.225.2": { - "integrity": "0f2db41d36b50ef048dcb0399aac720a5348638dd3cb5bf80685bf2a745aa506", - "dependencies": [ - "jsr:@std/assert@0.226" - ] - }, - "@std/path@1.0.0-rc.1": { - "integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6" - }, - "@std/path@1.0.0-rc.2": { - "integrity": "39f20d37a44d1867abac8d91c169359ea6e942237a45a99ee1e091b32b921c7d" - }, - "@std/path@1.1.3": { - "integrity": "b015962d82a5e6daea980c32b82d2c40142149639968549c649031a230b1afb3", - "dependencies": [ - "jsr:@std/internal" - ] - }, - "@std/path@1.1.4": { - "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", - "dependencies": [ - "jsr:@std/internal" - ] - }, - "@std/streams@1.0.17": { - "integrity": "7859f3d9deed83cf4b41f19223d4a67661b3d3819e9fc117698f493bf5992140", - "dependencies": [ - "jsr:@std/bytes@^1.0.6" - ] - }, - "@std/text@1.0.0-rc.1": { - "integrity": "34c722203e87ee12792c8d4a0cd2ee0e001341cbce75b860fc21be19d62232b0" - }, - "@std/yaml@1.0.10": { - "integrity": "245706ea3511cc50c8c6d00339c23ea2ffa27bd2c7ea5445338f8feff31fa58e" - }, - "@ts-morph/bootstrap@0.24.0": { - "integrity": "a826a2ef7fa8a7c3f1042df2c034d20744d94da2ee32bf29275bcd4dffd3c060", - "dependencies": [ - "jsr:@ts-morph/common@0.24" - ] - }, - "@ts-morph/bootstrap@0.27.0": { - "integrity": "b8d7bc8f7942ce853dde4161b28f9aa96769cef3d8eebafb379a81800b9e2448", - "dependencies": [ - "jsr:@ts-morph/common@0.27" - ] - }, - "@ts-morph/common@0.24.0": { - "integrity": "12b625b8e562446ba658cdbe9ad77774b4bd96b992ae8bd34c60dbf24d06c1f3", - "dependencies": [ - "jsr:@std/fs@~0.229.3", - "jsr:@std/path@~0.225.2" - ] - }, - "@ts-morph/common@0.27.0": { - "integrity": "c7b73592d78ce8479b356fd4f3d6ec3c460d77753a8680ff196effea7a939052", - "dependencies": [ - "jsr:@std/fs@1", - "jsr:@std/path@1" - ] - }, - "@windmill-labs/cliffy-ansi@1.0.0-rc.5": { - "integrity": "1109cbcb0c415b57779352f708f5969b8c645f56bc555cbafc6ea5e0c6a360a4", - "dependencies": [ - "jsr:@std/encoding@1.0.0-rc.2", - "jsr:@std/fmt@~0.225.4", - "jsr:@std/io@~0.224.2", - "jsr:@windmill-labs/cliffy-internal" - ] - }, - "@windmill-labs/cliffy-command@1.0.0-rc.5": { - "integrity": "3eaa9def5f5afa1028f4a60ee4d9065ccc5f194d032824365c6ebcb9f46db66e", - "dependencies": [ - "jsr:@std/fmt@~0.225.4", - "jsr:@std/text", - "jsr:@windmill-labs/cliffy-flags", - "jsr:@windmill-labs/cliffy-internal", - "jsr:@windmill-labs/cliffy-table@1.0.0-rc.5" - ] - }, - "@windmill-labs/cliffy-flags@1.0.0-rc.5": { - "integrity": "0e4b5b53a02295f8bf27d93b3bcca5d5d001a0286aea17404e6ef6347f69363c", - "dependencies": [ - "jsr:@std/text" - ] - }, - "@windmill-labs/cliffy-internal@1.0.0-rc.5": { - "integrity": "876b989ad2d1b739cc4a4f1386dbb80f819d2851ad583c1f486fc6ebe9beadf6" - }, - "@windmill-labs/cliffy-keycode@1.0.0-rc.5": { - "integrity": "2bc1b1af363528e38ed47bce525f417cab278b829a423353ffd589622f5a746e" - }, - "@windmill-labs/cliffy-prompt@1.0.0-rc.6": { - "integrity": "ffe09bee0e1e07bc12b2147be509d10b47eb6a36cbfea80fc206b4ae86693205", - "dependencies": [ - "jsr:@std/assert@1.0.0-rc.2", - "jsr:@std/fmt@~0.225.4", - "jsr:@std/io@~0.224.2", - "jsr:@std/path@1.0.0-rc.2", - "jsr:@std/text", - "jsr:@windmill-labs/cliffy-ansi@1.0.0-rc.5", - "jsr:@windmill-labs/cliffy-internal", - "jsr:@windmill-labs/cliffy-keycode" - ] - }, - "@windmill-labs/cliffy-table@1.0.0-rc.5": { - "integrity": "5f26cb6ccbc2fbf1b1f79f9062d166e32e11d34398c0deac7e6f9d8378970546", - "dependencies": [ - "jsr:@std/cli", - "jsr:@std/fmt@~0.225.4" - ] - }, - "@windmill-labs/shared-utils@1.0.3": { - "integrity": "35bafaf74092ebb63e96c75897337320378c04f93cf9b352fcc2137ffdb3e862" - }, - "@windmill-labs/shared-utils@1.0.5": { - "integrity": "3709140dc40f89443dff5953ec2e7c35d964b71c5e1245fba4072cf513e0db91" - }, - "@windmill-labs/shared-utils@1.0.6": { - "integrity": "34965cbc8e4fda69835fed37435468e8ca1123dabe4ea395d700ecdb2fa49738" - }, - "@windmill-labs/shared-utils@1.0.7": { - "integrity": "528638c7c508910e7f51b1ad9a5f1ff394e3fefb28fd3f96ab958c258a26e978" - }, - "@windmill-labs/shared-utils@1.0.10": { - "integrity": "bd1993eb8d693c8ba49da1618f82ff4601eeb59011b2cac13e664291f7a299d8" - }, - "@windmill-labs/shared-utils@1.0.11": { - "integrity": "4878a841480ad98213759495d72d40be1aebbbacc693f8aa9fc649127722580b" - }, - "@windmill-labs/shared-utils@1.0.12": { - "integrity": "fc9d19d42523fa99d19168b762ce0649b10f34d5889f948d70afe73278ce4381" - } - }, - "npm": { - "@ayonli/jsext@1.8.0": { - "integrity": "sha512-haJSYDLDaddK2LV1vr/n34lfLqIMdy0PH4+mumLBWMFzjJXhTXew9v6cpkaj9ZJhTbKRb+v+ny/0x3RxlkABZw==", - "dependencies": [ - "iconv-lite", - "sudo-prompt", - "ws@8.18.3", - "zod" - ] - }, - "@babel/helper-string-parser@7.27.1": { - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" - }, - "@babel/helper-validator-identifier@7.28.5": { - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==" - }, - "@babel/parser@7.28.5": { - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dependencies": [ - "@babel/types" - ], - "bin": true - }, - "@babel/types@7.28.5": { - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dependencies": [ - "@babel/helper-string-parser", - "@babel/helper-validator-identifier" - ] - }, - "@esbuild/aix-ppc64@0.24.2": { - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", - "os": ["aix"], - "cpu": ["ppc64"] - }, - "@esbuild/android-arm64@0.24.2": { - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", - "os": ["android"], - "cpu": ["arm64"] - }, - "@esbuild/android-arm@0.24.2": { - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", - "os": ["android"], - "cpu": ["arm"] - }, - "@esbuild/android-x64@0.24.2": { - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", - "os": ["android"], - "cpu": ["x64"] - }, - "@esbuild/darwin-arm64@0.24.2": { - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@esbuild/darwin-x64@0.24.2": { - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", - "os": ["darwin"], - "cpu": ["x64"] - }, - "@esbuild/freebsd-arm64@0.24.2": { - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", - "os": ["freebsd"], - "cpu": ["arm64"] - }, - "@esbuild/freebsd-x64@0.24.2": { - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", - "os": ["freebsd"], - "cpu": ["x64"] - }, - "@esbuild/linux-arm64@0.24.2": { - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@esbuild/linux-arm@0.24.2": { - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", - "os": ["linux"], - "cpu": ["arm"] - }, - "@esbuild/linux-ia32@0.24.2": { - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", - "os": ["linux"], - "cpu": ["ia32"] - }, - "@esbuild/linux-loong64@0.24.2": { - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", - "os": ["linux"], - "cpu": ["loong64"] - }, - "@esbuild/linux-mips64el@0.24.2": { - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", - "os": ["linux"], - "cpu": ["mips64el"] - }, - "@esbuild/linux-ppc64@0.24.2": { - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", - "os": ["linux"], - "cpu": ["ppc64"] - }, - "@esbuild/linux-riscv64@0.24.2": { - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", - "os": ["linux"], - "cpu": ["riscv64"] - }, - "@esbuild/linux-s390x@0.24.2": { - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", - "os": ["linux"], - "cpu": ["s390x"] - }, - "@esbuild/linux-x64@0.24.2": { - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@esbuild/netbsd-arm64@0.24.2": { - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", - "os": ["netbsd"], - "cpu": ["arm64"] - }, - "@esbuild/netbsd-x64@0.24.2": { - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", - "os": ["netbsd"], - "cpu": ["x64"] - }, - "@esbuild/openbsd-arm64@0.24.2": { - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", - "os": ["openbsd"], - "cpu": ["arm64"] - }, - "@esbuild/openbsd-x64@0.24.2": { - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", - "os": ["openbsd"], - "cpu": ["x64"] - }, - "@esbuild/sunos-x64@0.24.2": { - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", - "os": ["sunos"], - "cpu": ["x64"] - }, - "@esbuild/win32-arm64@0.24.2": { - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", - "os": ["win32"], - "cpu": ["arm64"] - }, - "@esbuild/win32-ia32@0.24.2": { - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", - "os": ["win32"], - "cpu": ["ia32"] - }, - "@esbuild/win32-x64@0.24.2": { - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", - "os": ["win32"], - "cpu": ["x64"] - }, - "@isaacs/balanced-match@4.0.1": { - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==" - }, - "@isaacs/brace-expansion@5.0.0": { - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dependencies": [ - "@isaacs/balanced-match" - ] - }, - "@jridgewell/gen-mapping@0.3.13": { - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dependencies": [ - "@jridgewell/sourcemap-codec", - "@jridgewell/trace-mapping" - ] - }, - "@jridgewell/remapping@2.3.5": { - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dependencies": [ - "@jridgewell/gen-mapping", - "@jridgewell/trace-mapping" - ] - }, - "@jridgewell/resolve-uri@3.1.2": { - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" - }, - "@jridgewell/sourcemap-codec@1.5.5": { - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" - }, - "@jridgewell/trace-mapping@0.3.31": { - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dependencies": [ - "@jridgewell/resolve-uri", - "@jridgewell/sourcemap-codec" - ] - }, - "@stoplight/ordered-object-literal@1.0.5": { - "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==" - }, - "@stoplight/types@14.1.1": { - "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", - "dependencies": [ - "@types/json-schema", - "utility-types" - ] - }, - "@stoplight/yaml-ast-parser@0.0.50": { - "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==" - }, - "@stoplight/yaml@4.3.0": { - "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==", - "dependencies": [ - "@stoplight/ordered-object-literal", - "@stoplight/types", - "@stoplight/yaml-ast-parser", - "tslib" - ] - }, - "@sveltejs/acorn-typescript@1.0.7_acorn@8.14.1": { - "integrity": "sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==", - "dependencies": [ - "acorn" - ] - }, - "@types/diff@5.2.3": { - "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==" - }, - "@types/estree@1.0.8": { - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" - }, - "@types/json-schema@7.0.15": { - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "@types/node@24.2.0": { - "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", - "dependencies": [ - "undici-types" - ] - }, - "@types/ws@8.18.1": { - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dependencies": [ - "@types/node" - ] - }, - "@vue/compiler-core@3.5.25": { - "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", - "dependencies": [ - "@babel/parser", - "@vue/shared", - "entities", - "estree-walker", - "source-map-js" - ] - }, - "@vue/compiler-dom@3.5.25": { - "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", - "dependencies": [ - "@vue/compiler-core", - "@vue/shared" - ] - }, - "@vue/compiler-sfc@3.5.25": { - "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", - "dependencies": [ - "@babel/parser", - "@vue/compiler-core", - "@vue/compiler-dom", - "@vue/compiler-ssr", - "@vue/shared", - "estree-walker", - "magic-string", - "postcss", - "source-map-js" - ] - }, - "@vue/compiler-ssr@3.5.25": { - "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", - "dependencies": [ - "@vue/compiler-dom", - "@vue/shared" - ] - }, - "@vue/reactivity@3.5.25": { - "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", - "dependencies": [ - "@vue/shared" - ] - }, - "@vue/runtime-core@3.5.25": { - "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", - "dependencies": [ - "@vue/reactivity", - "@vue/shared" - ] - }, - "@vue/runtime-dom@3.5.25": { - "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", - "dependencies": [ - "@vue/reactivity", - "@vue/runtime-core", - "@vue/shared", - "csstype" - ] - }, - "@vue/server-renderer@3.5.25_vue@3.5.25__typescript@4.9.5_typescript@4.9.5": { - "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", - "dependencies": [ - "@vue/compiler-ssr", - "@vue/shared", - "vue" - ] - }, - "@vue/shared@3.5.25": { - "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==" - }, - "@windmill-labs/shared-utils@1.0.1": { - "integrity": "sha512-DUMzPIFCKImuGpbuHXXmGGUT3VXYlgrv/jIIEOW+Iig+9tZvYqOUxfgn32lDhm73k82xBg8MdAf+0qABzfqFeQ==" - }, - "@windmill-labs/shared-utils@1.0.2": { - "integrity": "sha512-3LwALmwMeO3MqglGlyTtBUF05/ogpdDM5GiZKGN7271AEctS+ZJi3pXMHZ+YZdLxdgi2qLNNnVHO8qG5vFud2Q==" - }, - "accepts@2.0.0": { - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": [ - "mime-types", - "negotiator" - ] - }, - "acorn@8.14.1": { - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "bin": true - }, - "ajv@8.17.1": { - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dependencies": [ - "fast-deep-equal", - "fast-uri", - "json-schema-traverse", - "require-from-string" - ] - }, - "aria-query@5.3.2": { - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" - }, - "axobject-query@4.1.0": { - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" - }, - "body-parser@2.2.0": { - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dependencies": [ - "bytes", - "content-type", - "debug", - "http-errors", - "iconv-lite", - "on-finished", - "qs", - "raw-body", - "type-is" - ] - }, - "bundle-name@4.1.0": { - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dependencies": [ - "run-applescript" - ] - }, - "bytes@3.1.2": { - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind-apply-helpers@1.0.2": { - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": [ - "es-errors", - "function-bind" - ] - }, - "call-bound@1.0.4": { - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": [ - "call-bind-apply-helpers", - "get-intrinsic" - ] - }, - "centdix-utils@1.0.15": { - "integrity": "sha512-bf7a8yAzEiA7a64dQZPZoAt2uGF4m2POEOSyxha6qRUe0j0HVj+WmOuBkFmFJMQlBxQmBxhj2o6lxZ+NtSFyGQ==", - "dependencies": [ - "windmill-client" - ] - }, - "clsx@2.1.1": { - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" - }, - "content-disposition@1.0.0": { - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dependencies": [ - "safe-buffer@5.2.1" - ] - }, - "content-type@1.0.5": { - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, - "cookie-signature@1.2.2": { - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" - }, - "cookie@0.7.2": { - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" - }, - "core-util-is@1.0.3": { - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "csstype@3.2.3": { - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" - }, - "debug@4.4.1": { - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dependencies": [ - "ms" - ] - }, - "default-browser-id@5.0.0": { - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==" - }, - "default-browser@5.2.1": { - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "dependencies": [ - "bundle-name", - "default-browser-id" - ] - }, - "define-lazy-prop@3.0.0": { - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==" - }, - "depd@2.0.0": { - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "devalue@5.5.0": { - "integrity": "sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==" - }, - "diff@8.0.2": { - "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==" - }, - "dunder-proto@1.0.1": { - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": [ - "call-bind-apply-helpers", - "es-errors", - "gopd" - ] - }, - "ee-first@1.1.1": { - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "encodeurl@2.0.0": { - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" - }, - "entities@4.5.0": { - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" - }, - "es-define-property@1.0.1": { - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" - }, - "es-errors@1.3.0": { - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "es-main@1.3.0": { - "integrity": "sha512-AzORKdz1Zt97TzbYQnIrI3ZiibWpRXUfpo/w0xOJ20GpNYd2bd3MU9m31zS/aJ1TJl6JfLTok83Y8HjNunYT0A==" - }, - "es-object-atoms@1.1.1": { - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": [ - "es-errors" - ] - }, - "esbuild-plugin-vue3@0.5.1_vue@3.5.25__typescript@4.9.5_typescript@4.9.5": { - "integrity": "sha512-rhTPImJ1Zi7FbVa4xWlu9dJdt+mqWxc9Z+AQd+ArbHHwtyQRe8FvER8gaTw0O6bNsBjAtU5rq0rpZEkP3QaThg==", - "dependencies": [ - "typescript", - "vue" - ] - }, - "esbuild-svelte@0.9.3_esbuild@0.24.2_svelte@5.45.2__acorn@8.14.1": { - "integrity": "sha512-CgEcGY1r/d16+aggec3czoFBEBaYIrFOnMxpsO6fWNaNEqHregPN5DLAPZDqrL7rXDNplW+WMu8s3GMq9FqgJA==", - "dependencies": [ - "@jridgewell/trace-mapping", - "esbuild", - "svelte" - ] - }, - "esbuild@0.24.2": { - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", - "optionalDependencies": [ - "@esbuild/aix-ppc64", - "@esbuild/android-arm", - "@esbuild/android-arm64", - "@esbuild/android-x64", - "@esbuild/darwin-arm64", - "@esbuild/darwin-x64", - "@esbuild/freebsd-arm64", - "@esbuild/freebsd-x64", - "@esbuild/linux-arm", - "@esbuild/linux-arm64", - "@esbuild/linux-ia32", - "@esbuild/linux-loong64", - "@esbuild/linux-mips64el", - "@esbuild/linux-ppc64", - "@esbuild/linux-riscv64", - "@esbuild/linux-s390x", - "@esbuild/linux-x64", - "@esbuild/netbsd-arm64", - "@esbuild/netbsd-x64", - "@esbuild/openbsd-arm64", - "@esbuild/openbsd-x64", - "@esbuild/sunos-x64", - "@esbuild/win32-arm64", - "@esbuild/win32-ia32", - "@esbuild/win32-x64" - ], - "scripts": true, - "bin": true - }, - "escape-html@1.0.3": { - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "esm-env@1.2.2": { - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" - }, - "esrap@2.2.0": { - "integrity": "sha512-WBmtxe7R9C5mvL4n2le8nMUe4mD5V9oiK2vJpQ9I3y20ENPUomPcphBXE8D1x/Bm84oN1V+lOfgXxtqmxTp3Xg==", - "dependencies": [ - "@jridgewell/sourcemap-codec" - ] - }, - "estree-walker@2.0.2": { - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "etag@1.8.1": { - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "express@5.1.0": { - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dependencies": [ - "accepts", - "body-parser", - "content-disposition", - "content-type", - "cookie", - "cookie-signature", - "debug", - "encodeurl", - "escape-html", - "etag", - "finalhandler", - "fresh", - "http-errors", - "merge-descriptors", - "mime-types", - "on-finished", - "once", - "parseurl", - "proxy-addr", - "qs", - "range-parser", - "router", - "send", - "serve-static", - "statuses", - "type-is", - "vary" - ] - }, - "fast-deep-equal@3.1.3": { - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-uri@3.1.0": { - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==" - }, - "finalhandler@2.1.0": { - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dependencies": [ - "debug", - "encodeurl", - "escape-html", - "on-finished", - "parseurl", - "statuses" - ] - }, - "forwarded@0.2.0": { - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh@2.0.0": { - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" - }, - "function-bind@1.1.2": { - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-intrinsic@1.3.0": { - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": [ - "call-bind-apply-helpers", - "es-define-property", - "es-errors", - "es-object-atoms", - "function-bind", - "get-proto", - "gopd", - "has-symbols", - "hasown", - "math-intrinsics" - ] - }, - "get-port@7.1.0": { - "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==" - }, - "get-proto@1.0.1": { - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": [ - "dunder-proto", - "es-object-atoms" - ] - }, - "gopd@1.2.0": { - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" - }, - "has-symbols@1.1.0": { - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" - }, - "hasown@2.0.2": { - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": [ - "function-bind" - ] - }, - "http-errors@2.0.0": { - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": [ - "depd", - "inherits", - "setprototypeof", - "statuses", - "toidentifier" - ] - }, - "iconv-lite@0.6.3": { - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": [ - "safer-buffer" - ] - }, - "immediate@3.0.6": { - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "inherits@2.0.4": { - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js@1.9.1": { - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-docker@3.0.0": { - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "bin": true - }, - "is-inside-container@1.0.0": { - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dependencies": [ - "is-docker" - ], - "bin": true - }, - "is-promise@4.0.0": { - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "is-reference@3.0.3": { - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dependencies": [ - "@types/estree" - ] - }, - "is-wsl@3.1.0": { - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dependencies": [ - "is-inside-container" - ] - }, - "isarray@1.0.0": { - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "json-schema-traverse@1.0.0": { - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "jszip@3.7.1": { - "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", - "dependencies": [ - "lie", - "pako", - "readable-stream", - "set-immediate-shim" - ] - }, - "jszip@3.8.0": { - "integrity": "sha512-cnpQrXvFSLdsR9KR5/x7zdf6c3m8IhZfZzSblFEHSqBaVwD2nvJ4CuCKLyvKvwBgZm08CgfSoiTBQLm5WW9hGw==", - "dependencies": [ - "lie", - "pako", - "readable-stream", - "set-immediate-shim" - ] - }, - "lie@3.3.0": { - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dependencies": [ - "immediate" - ] - }, - "locate-character@3.0.0": { - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" - }, - "magic-string@0.30.21": { - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dependencies": [ - "@jridgewell/sourcemap-codec" - ] - }, - "math-intrinsics@1.1.0": { - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" - }, - "media-typer@1.1.0": { - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" - }, - "merge-descriptors@2.0.0": { - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" - }, - "mime-db@1.54.0": { - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" - }, - "mime-types@3.0.1": { - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": [ - "mime-db" - ] - }, - "minimatch@10.0.3": { - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "dependencies": [ - "@isaacs/brace-expansion" - ] - }, - "ms@2.1.3": { - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "nanoid@3.3.11": { - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "bin": true - }, - "negotiator@1.0.0": { - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" - }, - "object-inspect@1.13.4": { - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" - }, - "on-finished@2.4.1": { - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": [ - "ee-first" - ] - }, - "once@1.4.0": { - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": [ - "wrappy" - ] - }, - "open@10.2.0": { - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "dependencies": [ - "default-browser", - "define-lazy-prop", - "is-inside-container", - "wsl-utils" - ] - }, - "pako@1.0.11": { - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "parseurl@1.3.3": { - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-to-regexp@8.2.0": { - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==" - }, - "picocolors@1.1.1": { - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "postcss@8.5.6": { - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dependencies": [ - "nanoid", - "picocolors", - "source-map-js" - ] - }, - "process-nextick-args@2.0.1": { - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "proxy-addr@2.0.7": { - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": [ - "forwarded", - "ipaddr.js" - ] - }, - "qs@6.14.0": { - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dependencies": [ - "side-channel" - ] - }, - "range-parser@1.2.1": { - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body@3.0.0": { - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dependencies": [ - "bytes", - "http-errors", - "iconv-lite", - "unpipe" - ] - }, - "readable-stream@2.3.8": { - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": [ - "core-util-is", - "inherits", - "isarray", - "process-nextick-args", - "safe-buffer@5.1.2", - "string_decoder", - "util-deprecate" - ] - }, - "require-from-string@2.0.2": { - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" - }, - "router@2.2.0": { - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dependencies": [ - "debug", - "depd", - "is-promise", - "parseurl", - "path-to-regexp" - ] - }, - "run-applescript@7.0.0": { - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==" - }, - "safe-buffer@5.1.2": { - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-buffer@5.2.1": { - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer@2.1.2": { - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "send@1.2.0": { - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dependencies": [ - "debug", - "encodeurl", - "escape-html", - "etag", - "fresh", - "http-errors", - "mime-types", - "ms", - "on-finished", - "range-parser", - "statuses" - ] - }, - "serve-static@2.2.0": { - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dependencies": [ - "encodeurl", - "escape-html", - "parseurl", - "send" - ] - }, - "set-immediate-shim@1.0.1": { - "integrity": "sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ==" - }, - "setprototypeof@1.2.0": { - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "side-channel-list@1.0.0": { - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dependencies": [ - "es-errors", - "object-inspect" - ] - }, - "side-channel-map@1.0.1": { - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": [ - "call-bound", - "es-errors", - "get-intrinsic", - "object-inspect" - ] - }, - "side-channel-weakmap@1.0.2": { - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": [ - "call-bound", - "es-errors", - "get-intrinsic", - "object-inspect", - "side-channel-map" - ] - }, - "side-channel@1.1.0": { - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": [ - "es-errors", - "object-inspect", - "side-channel-list", - "side-channel-map", - "side-channel-weakmap" - ] - }, - "source-map-js@1.2.1": { - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" - }, - "statuses@2.0.1": { - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "string_decoder@1.1.1": { - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": [ - "safe-buffer@5.1.2" - ] - }, - "sudo-prompt@9.2.1": { - "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", - "deprecated": true - }, - "svelte-preprocess@6.0.3_svelte@5.45.2__acorn@8.14.1": { - "integrity": "sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==", - "dependencies": [ - "svelte" - ], - "scripts": true - }, - "svelte@5.45.2_acorn@8.14.1": { - "integrity": "sha512-yyXdW2u3H0H/zxxWoGwJoQlRgaSJLp+Vhktv12iRw2WRDlKqUPT54Fi0K/PkXqrdkcQ98aBazpy0AH4BCBVfoA==", - "dependencies": [ - "@jridgewell/remapping", - "@jridgewell/sourcemap-codec", - "@sveltejs/acorn-typescript", - "@types/estree", - "acorn", - "aria-query", - "axobject-query", - "clsx", - "devalue", - "esm-env", - "esrap", - "is-reference", - "locate-character", - "magic-string", - "zimmerframe" - ] - }, - "toidentifier@1.0.1": { - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "tslib@2.8.1": { - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "type-is@2.0.1": { - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dependencies": [ - "content-type", - "media-typer", - "mime-types" - ] - }, - "typescript@4.9.5": { - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "bin": true - }, - "undici-types@7.10.0": { - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" - }, - "unpipe@1.0.0": { - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "util-deprecate@1.0.2": { - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "utility-types@3.11.0": { - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==" - }, - "vary@1.1.2": { - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, - "vue@3.5.25_typescript@4.9.5": { - "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", - "dependencies": [ - "@vue/compiler-dom", - "@vue/compiler-sfc", - "@vue/runtime-dom", - "@vue/server-renderer", - "@vue/shared", - "typescript" - ], - "optionalPeers": [ - "typescript" - ] - }, - "windmill-client@1.515.1": { - "integrity": "sha512-o6qynOEbPubZTZUOLLs2Z9f+uBZQJUCw/+YWgvI6p8nu5BJ6J3N/wEfbY1X5TTnJNuqahQ0UgimYzhurT5XQFw==" - }, - "windmill-yaml-validator@1.1.0": { - "integrity": "sha512-TM9rl6NycP4eXYOzi4Y8/EXHU4phzFUJWN28IlHDx4eRDNPEkj+6jAF4xUaBvLeFEJl0CfznEdhtM61vDgomKQ==", - "dependencies": [ - "@stoplight/yaml", - "ajv" - ] - }, - "windmill-yaml-validator@1.1.1": { - "integrity": "sha512-CVgAwEoBdJhF39q2N012QffhlGPRIyIWd8gj7NnfG+/lMWgH2k5CBLtKIt6cPF8Bxz+6DGC3st1ARSsecDtbTg==", - "dependencies": [ - "@stoplight/yaml", - "ajv" - ] - }, - "wrappy@1.0.2": { - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "ws@8.18.0": { - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==" - }, - "ws@8.18.3": { - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==" - }, - "wsl-utils@0.1.0": { - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "dependencies": [ - "is-wsl" - ] - }, - "zimmerframe@1.1.4": { - "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==" - }, - "zod@3.25.76": { - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" - } - }, - "remote": { - "https://deno.land/std@0.207.0/yaml/_dumper/dumper.ts": "717403d0e700de783f2ef5c906b3d7245383e1509fc050e7ff5d4a53a03dbf40", - "https://deno.land/std@0.207.0/yaml/_dumper/dumper_state.ts": "f0d0673ceea288334061ca34b63954c2bb5feb5bf6de5e4cfe9a942cdf6e5efe", - "https://deno.land/std@0.207.0/yaml/_error.ts": "b59e2c76ce5a47b1b9fa0ff9f96c1dd92ea1e1b17ce4347ece5944a95c3c1a84", - "https://deno.land/std@0.207.0/yaml/_loader/loader.ts": "63ec7f0a265dbbabc54b25a4beefff7650e205160a2d75c7d8f8363b5f84851a", - "https://deno.land/std@0.207.0/yaml/_loader/loader_state.ts": "0841870b467169269d7c2dfa75cd288c319bc06f65edd9e42c29e5fced91c7a4", - "https://deno.land/std@0.207.0/yaml/_mark.ts": "dcd8585dee585e024475e9f3fe27d29740670fb64ebb970388094cad0fc11d5d", - "https://deno.land/std@0.207.0/yaml/_state.ts": "ef03d55ec235d48dcfbecc0ab3ade90bfae69a61094846e08003421c2cf5cfc6", - "https://deno.land/std@0.207.0/yaml/_type/binary.ts": "24d49614463a7339a8a16d894919c2ec18a10588ae360ec352093b60e2cc8b0d", - "https://deno.land/std@0.207.0/yaml/_type/bool.ts": "5bfa75da84343d45347b521ba4e5aeace9fe6f53447405290d53315a3fc20e66", - "https://deno.land/std@0.207.0/yaml/_type/float.ts": "056bd3cb9c5586238b20517511014fb24b0e36f98f9f6073e12da308b6b9808a", - "https://deno.land/std@0.207.0/yaml/_type/function.ts": "ff574fe84a750695302864e1c31b93f12d14ada4bde79a5f93197fc33ad17471", - "https://deno.land/std@0.207.0/yaml/_type/int.ts": "563ad074f0fa7aecf6b6c3d84135bcc95a8269dcc15de878de20ce868fd773fa", - "https://deno.land/std@0.207.0/yaml/_type/map.ts": "7b105e4ab03a361c61e7e335a0baf4d40f06460b13920e5af3fb2783a1464000", - "https://deno.land/std@0.207.0/yaml/_type/merge.ts": "8192bf3e4d637f32567917f48bb276043da9cf729cf594e5ec191f7cd229337e", - "https://deno.land/std@0.207.0/yaml/_type/mod.ts": "060e2b3d38725094b77ea3a3f05fc7e671fced8e67ca18e525be98c4aa8f4bbb", - "https://deno.land/std@0.207.0/yaml/_type/nil.ts": "606e8f0c44d73117c81abec822f89ef81e40f712258c74f186baa1af659b8887", - "https://deno.land/std@0.207.0/yaml/_type/omap.ts": "cfe59a294726f5cea705c39a61fd2b08199cf48f4ccd6b040cb550ec0f38d0a1", - "https://deno.land/std@0.207.0/yaml/_type/pairs.ts": "0032fdfe57558d21696a4f8cf5b5cfd1f698743177080affc18629685c905666", - "https://deno.land/std@0.207.0/yaml/_type/regexp.ts": "1ce118de15b2da43b4bd8e4395f42d448b731acf3bdaf7c888f40789f9a95f8b", - "https://deno.land/std@0.207.0/yaml/_type/seq.ts": "95333abeec8a7e4d967b8c8328b269e342a4bbdd2585395549b9c4f58c8533a2", - "https://deno.land/std@0.207.0/yaml/_type/set.ts": "f28ba44e632ef2a6eb580486fd47a460445eeddbdf1dbc739c3e62486f566092", - "https://deno.land/std@0.207.0/yaml/_type/str.ts": "a67a3c6e429d95041399e964015511779b1130ea5889fa257c48457bd3446e31", - "https://deno.land/std@0.207.0/yaml/_type/timestamp.ts": "706ea80a76a73e48efaeb400ace087da1f927647b53ad6f754f4e06d51af087f", - "https://deno.land/std@0.207.0/yaml/_type/undefined.ts": "94a316ca450597ccbc6750cbd79097ad0d5f3a019797eed3c841a040c29540ba", - "https://deno.land/std@0.207.0/yaml/_utils.ts": "26b311f0d42a7ce025060bd6320a68b50e52fd24a839581eb31734cd48e20393", - "https://deno.land/std@0.207.0/yaml/mod.ts": "28ecda6652f3e7a7735ee29c247bfbd32a2e2fc5724068e9fd173ec4e59f66f7", - "https://deno.land/std@0.207.0/yaml/parse.ts": "1fbbda572bf3fff578b6482c0d8b85097a38de3176bf3ab2ca70c25fb0c960ef", - "https://deno.land/std@0.207.0/yaml/schema.ts": "96908b78dc50c340074b93fc1598d5e7e2fe59103f89ff81e5a49b2dedf77a67", - "https://deno.land/std@0.207.0/yaml/schema/core.ts": "fa406f18ceedc87a50e28bb90ec7a4c09eebb337f94ef17468349794fa828639", - "https://deno.land/std@0.207.0/yaml/schema/default.ts": "0047e80ae8a4a93293bc4c557ae8a546aabd46bb7165b9d9b940d57b4d88bde9", - "https://deno.land/std@0.207.0/yaml/schema/extended.ts": "0784416bf062d20a1626b53c03380e265b3e39b9409afb9f4cb7d659fd71e60d", - "https://deno.land/std@0.207.0/yaml/schema/failsafe.ts": "d219ab5febc43f770917d8ec37735a4b1ad671149846cbdcade767832b42b92b", - "https://deno.land/std@0.207.0/yaml/schema/json.ts": "5f41dd7c2f1ad545ef6238633ce9ee3d444dfc5a18101e1768bd5504bf90e5e5", - "https://deno.land/std@0.207.0/yaml/schema/mod.ts": "4472e827bab5025e92bc2eb2eeefa70ecbefc64b2799b765c69af84822efef32", - "https://deno.land/std@0.207.0/yaml/stringify.ts": "fffc09c65c68d3d63f8159e8cbaa3f489bc20a8e55b4fbb61a8c2e9f914d1d02", - "https://deno.land/std@0.207.0/yaml/type.ts": "65553da3da3c029b6589c6e4903f0afbea6768be8fca61580711457151f2b30f", - "https://deno.land/std@0.208.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", - "https://deno.land/std@0.208.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48", - "https://deno.land/std@0.208.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", - "https://deno.land/std@0.208.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", - "https://deno.land/std@0.208.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", - "https://deno.land/std@0.208.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", - "https://deno.land/std@0.208.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", - "https://deno.land/std@0.208.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", - "https://deno.land/std@0.208.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6", - "https://deno.land/std@0.208.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", - "https://deno.land/std@0.208.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", - "https://deno.land/std@0.208.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", - "https://deno.land/std@0.208.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", - "https://deno.land/std@0.208.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", - "https://deno.land/std@0.208.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", - "https://deno.land/std@0.208.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", - "https://deno.land/std@0.208.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", - "https://deno.land/std@0.208.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", - "https://deno.land/std@0.208.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", - "https://deno.land/std@0.208.0/assert/assert_not_strict_equals.ts": "4cdef83df17488df555c8aac1f7f5ec2b84ad161b6d0645ccdbcc17654e80c99", - "https://deno.land/std@0.208.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", - "https://deno.land/std@0.208.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", - "https://deno.land/std@0.208.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", - "https://deno.land/std@0.208.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", - "https://deno.land/std@0.208.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", - "https://deno.land/std@0.208.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", - "https://deno.land/std@0.208.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", - "https://deno.land/std@0.208.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", - "https://deno.land/std@0.208.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", - "https://deno.land/std@0.208.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", - "https://deno.land/std@0.208.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", - "https://deno.land/std@0.208.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2", - "https://deno.land/std@0.208.0/path/_common/assert_path.ts": "061e4d093d4ba5aebceb2c4da3318bfe3289e868570e9d3a8e327d91c2958946", - "https://deno.land/std@0.208.0/path/_common/basename.ts": "0d978ff818f339cd3b1d09dc914881f4d15617432ae519c1b8fdc09ff8d3789a", - "https://deno.land/std@0.208.0/path/_common/common.ts": "9e4233b2eeb50f8b2ae10ecc2108f58583aea6fd3e8907827020282dc2b76143", - "https://deno.land/std@0.208.0/path/_common/constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", - "https://deno.land/std@0.208.0/path/_common/dirname.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397", - "https://deno.land/std@0.208.0/path/_common/format.ts": "11aa62e316dfbf22c126917f5e03ea5fe2ee707386555a8f513d27ad5756cf96", - "https://deno.land/std@0.208.0/path/_common/from_file_url.ts": "ef1bf3197d2efbf0297a2bdbf3a61d804b18f2bcce45548ae112313ec5be3c22", - "https://deno.land/std@0.208.0/path/_common/glob_to_reg_exp.ts": "5c3c2b79fc2294ec803d102bd9855c451c150021f452046312819fbb6d4dc156", - "https://deno.land/std@0.208.0/path/_common/normalize.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397", - "https://deno.land/std@0.208.0/path/_common/normalize_string.ts": "88c472f28ae49525f9fe82de8c8816d93442d46a30d6bb5063b07ff8a89ff589", - "https://deno.land/std@0.208.0/path/_common/relative.ts": "1af19d787a2a84b8c534cc487424fe101f614982ae4851382c978ab2216186b4", - "https://deno.land/std@0.208.0/path/_common/strip_trailing_separators.ts": "7ffc7c287e97bdeeee31b155828686967f222cd73f9e5780bfe7dfb1b58c6c65", - "https://deno.land/std@0.208.0/path/_common/to_file_url.ts": "a8cdd1633bc9175b7eebd3613266d7c0b6ae0fb0cff24120b6092ac31662f9ae", - "https://deno.land/std@0.208.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", - "https://deno.land/std@0.208.0/path/_os.ts": "30b0c2875f360c9296dbe6b7f2d528f0f9c741cecad2e97f803f5219e91b40a2", - "https://deno.land/std@0.208.0/path/basename.ts": "04bb5ef3e86bba8a35603b8f3b69537112cdd19ce64b77f2522006da2977a5f3", - "https://deno.land/std@0.208.0/path/common.ts": "f4d061c7d0b95a65c2a1a52439edec393e906b40f1caf4604c389fae7caa80f5", - "https://deno.land/std@0.208.0/path/dirname.ts": "88a0a71c21debafc4da7a4cd44fd32e899462df458fbca152390887d41c40361", - "https://deno.land/std@0.208.0/path/extname.ts": "2da4e2490f3b48b7121d19fb4c91681a5e11bd6bd99df4f6f47d7a71bb6ecdf2", - "https://deno.land/std@0.208.0/path/format.ts": "3457530cc85d1b4bab175f9ae73998b34fd456c830d01883169af0681b8894fb", - "https://deno.land/std@0.208.0/path/from_file_url.ts": "e7fa233ea1dff9641e8d566153a24d95010110185a6f418dd2e32320926043f8", - "https://deno.land/std@0.208.0/path/glob_to_regexp.ts": "74d7448c471e293d03f05ccb968df4365fed6aaa508506b6325a8efdc01d8271", - "https://deno.land/std@0.208.0/path/is_absolute.ts": "67232b41b860571c5b7537f4954c88d86ae2ba45e883ee37d3dec27b74909d13", - "https://deno.land/std@0.208.0/path/is_glob.ts": "567dce5c6656bdedfc6b3ee6c0833e1e4db2b8dff6e62148e94a917f289c06ad", - "https://deno.land/std@0.208.0/path/join.ts": "98d3d76c819af4a11a81d5ba2dbb319f1ce9d63fc2b615597d4bcfddd4a89a09", - "https://deno.land/std@0.208.0/path/join_globs.ts": "9b84d5103b63d3dbed4b2cf8b12477b2ad415c7d343f1488505162dc0e5f4db8", - "https://deno.land/std@0.208.0/path/mod.ts": "3defabebc98279e62b392fee7a6937adc932a8f4dcd2471441e36c15b97b00e0", - "https://deno.land/std@0.208.0/path/normalize.ts": "aa95be9a92c7bd4f9dc0ba51e942a1973e2b93d266cd74f5ca751c136d520b66", - "https://deno.land/std@0.208.0/path/normalize_glob.ts": "674baa82e1c00b6cb153bbca36e06f8e0337cb8062db6d905ab5de16076ca46b", - "https://deno.land/std@0.208.0/path/parse.ts": "d87ff0deef3fb495bc0d862278ff96da5a06acf0625ca27769fc52ac0d3d6ece", - "https://deno.land/std@0.208.0/path/posix/_util.ts": "ecf49560fedd7dd376c6156cc5565cad97c1abe9824f4417adebc7acc36c93e5", - "https://deno.land/std@0.208.0/path/posix/basename.ts": "a630aeb8fd8e27356b1823b9dedd505e30085015407caa3396332752f6b8406a", - "https://deno.land/std@0.208.0/path/posix/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b", - "https://deno.land/std@0.208.0/path/posix/dirname.ts": "f48c9c42cc670803b505478b7ef162c7cfa9d8e751b59d278b2ec59470531472", - "https://deno.land/std@0.208.0/path/posix/extname.ts": "ee7f6571a9c0a37f9218fbf510c440d1685a7c13082c348d701396cc795e0be0", - "https://deno.land/std@0.208.0/path/posix/format.ts": "b94876f77e61bfe1f147d5ccb46a920636cd3cef8be43df330f0052b03875968", - "https://deno.land/std@0.208.0/path/posix/from_file_url.ts": "b97287a83e6407ac27bdf3ab621db3fccbf1c27df0a1b1f20e1e1b5acf38a379", - "https://deno.land/std@0.208.0/path/posix/glob_to_regexp.ts": "6ed00c71fbfe0ccc35977c35444f94e82200b721905a60bd1278b1b768d68b1a", - "https://deno.land/std@0.208.0/path/posix/is_absolute.ts": "159900a3422d11069d48395568217eb7fc105ceda2683d03d9b7c0f0769e01b8", - "https://deno.land/std@0.208.0/path/posix/is_glob.ts": "ec4fbc604b9db8487f7b56ab0e759b24a971ab6a45f7b0b698bc39b8b9f9680f", - "https://deno.land/std@0.208.0/path/posix/join.ts": "0c0d84bdc344876930126640011ec1b888e6facf74153ffad9ef26813aa2a076", - "https://deno.land/std@0.208.0/path/posix/join_globs.ts": "f4838d54b1f60a34a40625a3293f6e583135348be1b2974341ac04743cb26121", - "https://deno.land/std@0.208.0/path/posix/mod.ts": "f1b08a7f64294b7de87fc37190d63b6ce5b02889af9290c9703afe01951360ae", - "https://deno.land/std@0.208.0/path/posix/normalize.ts": "11de90a94ab7148cc46e5a288f7d732aade1d616bc8c862f5560fa18ff987b4b", - "https://deno.land/std@0.208.0/path/posix/normalize_glob.ts": "10a1840c628ebbab679254d5fa1c20e59106102354fb648a1765aed72eb9f3f9", - "https://deno.land/std@0.208.0/path/posix/parse.ts": "199208f373dd93a792e9c585352bfc73a6293411bed6da6d3bc4f4ef90b04c8e", - "https://deno.land/std@0.208.0/path/posix/relative.ts": "e2f230608b0f083e6deaa06e063943e5accb3320c28aef8d87528fbb7fe6504c", - "https://deno.land/std@0.208.0/path/posix/resolve.ts": "51579d83159d5c719518c9ae50812a63959bbcb7561d79acbdb2c3682236e285", - "https://deno.land/std@0.208.0/path/posix/separator.ts": "0b6573b5f3269a3164d8edc9cefc33a02dd51003731c561008c8bb60220ebac1", - "https://deno.land/std@0.208.0/path/posix/to_file_url.ts": "08d43ea839ee75e9b8b1538376cfe95911070a655cd312bc9a00f88ef14967b6", - "https://deno.land/std@0.208.0/path/posix/to_namespaced_path.ts": "c9228a0e74fd37e76622cd7b142b8416663a9b87db643302fa0926b5a5c83bdc", - "https://deno.land/std@0.208.0/path/relative.ts": "23d45ede8b7ac464a8299663a43488aad6b561414e7cbbe4790775590db6349c", - "https://deno.land/std@0.208.0/path/resolve.ts": "5b184efc87155a0af9fa305ff68a109e28de9aee81fc3e77cd01380f19daf867", - "https://deno.land/std@0.208.0/path/separator.ts": "40a3e9a4ad10bef23bc2cd6c610291b6c502a06237c2c4cd034a15ca78dedc1f", - "https://deno.land/std@0.208.0/path/to_file_url.ts": "edaafa089e0bce386e1b2d47afe7c72e379ff93b28a5829a5885e4b6c626d864", - "https://deno.land/std@0.208.0/path/to_namespaced_path.ts": "cf8734848aac3c7527d1689d2adf82132b1618eff3cc523a775068847416b22a", - "https://deno.land/std@0.208.0/path/windows/_util.ts": "f32b9444554c8863b9b4814025c700492a2b57ff2369d015360970a1b1099d54", - "https://deno.land/std@0.208.0/path/windows/basename.ts": "8a9dbf7353d50afbc5b221af36c02a72c2d1b2b5b9f7c65bf6a5a2a0baf88ad3", - "https://deno.land/std@0.208.0/path/windows/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b", - "https://deno.land/std@0.208.0/path/windows/dirname.ts": "5c2aa541384bf0bd9aca821275d2a8690e8238fa846198ef5c7515ce31a01a94", - "https://deno.land/std@0.208.0/path/windows/extname.ts": "07f4fa1b40d06a827446b3e3bcc8d619c5546b079b8ed0c77040bbef716c7614", - "https://deno.land/std@0.208.0/path/windows/format.ts": "343019130d78f172a5c49fdc7e64686a7faf41553268961e7b6c92a6d6548edf", - "https://deno.land/std@0.208.0/path/windows/from_file_url.ts": "d53335c12b0725893d768be3ac6bf0112cc5b639d2deb0171b35988493b46199", - "https://deno.land/std@0.208.0/path/windows/glob_to_regexp.ts": "290755e18ec6c1a4f4d711c3390537358e8e3179581e66261a0cf348b1a13395", - "https://deno.land/std@0.208.0/path/windows/is_absolute.ts": "245b56b5f355ede8664bd7f080c910a97e2169972d23075554ae14d73722c53c", - "https://deno.land/std@0.208.0/path/windows/is_glob.ts": "ec4fbc604b9db8487f7b56ab0e759b24a971ab6a45f7b0b698bc39b8b9f9680f", - "https://deno.land/std@0.208.0/path/windows/join.ts": "e6600bf88edeeef4e2276e155b8de1d5dec0435fd526ba2dc4d37986b2882f16", - "https://deno.land/std@0.208.0/path/windows/join_globs.ts": "f4838d54b1f60a34a40625a3293f6e583135348be1b2974341ac04743cb26121", - "https://deno.land/std@0.208.0/path/windows/mod.ts": "d7040f461465c2c21c1c68fc988ef0bdddd499912138cde3abf6ad60c7fb3814", - "https://deno.land/std@0.208.0/path/windows/normalize.ts": "9deebbf40c81ef540b7b945d4ccd7a6a2c5a5992f791e6d3377043031e164e69", - "https://deno.land/std@0.208.0/path/windows/normalize_glob.ts": "344ff5ed45430495b9a3d695567291e50e00b1b3b04ea56712a2acf07ab5c128", - "https://deno.land/std@0.208.0/path/windows/parse.ts": "120faf778fe1f22056f33ded069b68e12447668fcfa19540c0129561428d3ae5", - "https://deno.land/std@0.208.0/path/windows/relative.ts": "026855cd2c36c8f28f1df3c6fbd8f2449a2aa21f48797a74700c5d872b86d649", - "https://deno.land/std@0.208.0/path/windows/resolve.ts": "5ff441ab18a2346abadf778121128ee71bda4d0898513d4639a6ca04edca366b", - "https://deno.land/std@0.208.0/path/windows/separator.ts": "ae21f27015f10510ed1ac4a0ba9c4c9c967cbdd9d9e776a3e4967553c397bd5d", - "https://deno.land/std@0.208.0/path/windows/to_file_url.ts": "8e9ea9e1ff364aa06fa72999204229952d0a279dbb876b7b838b2b2fea55cce3", - "https://deno.land/std@0.208.0/path/windows/to_namespaced_path.ts": "e0f4d4a5e77f28a5708c1a33ff24360f35637ba6d8f103d19661255ef7bfd50d", - "https://deno.land/std@0.208.0/testing/asserts.ts": "605bbd2ef0695e2a4324d810c4ad22e56041d51afb9584fc0b4e81084b14b1d6", - "https://deno.land/std@0.213.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", - "https://deno.land/std@0.213.0/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840", - "https://deno.land/std@0.213.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4", - "https://deno.land/std@0.213.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", - "https://deno.land/std@0.213.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f", - "https://deno.land/std@0.213.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1", - "https://deno.land/std@0.213.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e", - "https://deno.land/std@0.213.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9", - "https://deno.land/std@0.213.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769", - "https://deno.land/std@0.213.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c", - "https://deno.land/std@0.213.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219", - "https://deno.land/std@0.213.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444", - "https://deno.land/std@0.213.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2", - "https://deno.land/std@0.213.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005", - "https://deno.land/std@0.213.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0", - "https://deno.land/std@0.213.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1", - "https://deno.land/std@0.213.0/assert/assert_not_equals.ts": "f3edda73043bc2c9fae6cbfaa957d5c69bbe76f5291a5b0466ed132c8789df4c", - "https://deno.land/std@0.213.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931", - "https://deno.land/std@0.213.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f", - "https://deno.land/std@0.213.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be", - "https://deno.land/std@0.213.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49", - "https://deno.land/std@0.213.0/assert/assert_rejects.ts": "e9e0c8d9c3e164c7ac962c37b3be50577c5a2010db107ed272c4c1afb1269f54", - "https://deno.land/std@0.213.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366", - "https://deno.land/std@0.213.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7", - "https://deno.land/std@0.213.0/assert/assert_throws.ts": "edddd86b39606c342164b49ad88dd39a26e72a26655e07545d172f164b617fa7", - "https://deno.land/std@0.213.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", - "https://deno.land/std@0.213.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2", - "https://deno.land/std@0.213.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c", - "https://deno.land/std@0.213.0/assert/mod.ts": "325df8c0683ad83a873b9691aa66b812d6275fc9fec0b2d180ac68a2c5efed3b", - "https://deno.land/std@0.213.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd", - "https://deno.land/std@0.213.0/assert/unreachable.ts": "38cfecb95d8b06906022d2f9474794fca4161a994f83354fd079cac9032b5145", - "https://deno.land/std@0.213.0/fmt/colors.ts": "aeaee795471b56fc62a3cb2e174ed33e91551b535f44677f6320336aabb54fbb", - "https://deno.land/std@0.213.0/testing/_test_suite.ts": "f10a8a6338b60c403f07a76f3f46bdc9f1e1a820c0a1decddeb2949f7a8a0546", - "https://deno.land/std@0.213.0/testing/bdd.ts": "3cbd17bd35f629a76ce63446238dfb4632240dd46b3b205027c45fa3dd67e554", - "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", - "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", - "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", - "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", - "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", - "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", - "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", - "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", - "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", - "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", - "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", - "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", - "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", - "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", - "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", - "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", - "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", - "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", - "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", - "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", - "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", - "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", - "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", - "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", - "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", - "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", - "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", - "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", - "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", - "https://deno.land/std@0.224.0/cli/parse_args.ts": "5250832fb7c544d9111e8a41ad272c016f5a53f975ef84d5a9fe5fcb70566ece", - "https://deno.land/std@0.224.0/encoding/_util.ts": "beacef316c1255da9bc8e95afb1fa56ed69baef919c88dc06ae6cb7a6103d376", - "https://deno.land/std@0.224.0/encoding/hex.ts": "6270f25e5d85f99fcf315278670ba012b04b7c94b67715b53f30d03249687c07", - "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", - "https://deno.land/std@0.224.0/fs/_create_walk_entry.ts": "5d9d2aaec05bcf09a06748b1684224d33eba7a4de24cf4cf5599991ca6b5b412", - "https://deno.land/std@0.224.0/fs/_get_file_info_type.ts": "da7bec18a7661dba360a1db475b826b18977582ce6fc9b25f3d4ee0403fe8cbd", - "https://deno.land/std@0.224.0/fs/_is_same_path.ts": "709c95868345fea051c58b9e96af95cff94e6ae98dfcff2b66dee0c212c4221f", - "https://deno.land/std@0.224.0/fs/_is_subdir.ts": "c68b309d46cc8568ed83c000f608a61bbdba0943b7524e7a30f9e450cf67eecd", - "https://deno.land/std@0.224.0/fs/_to_path_string.ts": "29bfc9c6c112254961d75cbf6ba814d6de5349767818eb93090cecfa9665591e", - "https://deno.land/std@0.224.0/fs/copy.ts": "7ab12a16adb65d155d4943c88081ca16ce3b0b5acada64c1ce93800653678039", - "https://deno.land/std@0.224.0/fs/empty_dir.ts": "e400e96e1d2c8c558a5a1712063bd43939e00619c1d1cc29959babc6f1639418", - "https://deno.land/std@0.224.0/fs/ensure_dir.ts": "51a6279016c65d2985f8803c848e2888e206d1b510686a509fa7cc34ce59d29f", - "https://deno.land/std@0.224.0/fs/ensure_file.ts": "67608cf550529f3d4aa1f8b6b36bf817bdc40b14487bf8f60e61cbf68f507cf3", - "https://deno.land/std@0.224.0/fs/ensure_link.ts": "5c98503ebfa9cc05e2f2efaa30e91e60b4dd5b43ebbda82f435c0a5c6e3ffa01", - "https://deno.land/std@0.224.0/fs/ensure_symlink.ts": "cafe904cebacb9a761977d6dbf5e3af938be946a723bb394080b9a52714fafe4", - "https://deno.land/std@0.224.0/fs/eol.ts": "18c4ac009d0318504c285879eb7f47942643f13619e0ff070a0edc59353306bd", - "https://deno.land/std@0.224.0/fs/exists.ts": "3d38cb7dcbca3cf313be343a7b8af18a87bddb4b5ca1bd2314be12d06533b50f", - "https://deno.land/std@0.224.0/fs/expand_glob.ts": "2e428d90acc6676b2aa7b5c78ef48f30641b13f1fe658e7976c9064fb4b05309", - "https://deno.land/std@0.224.0/fs/mod.ts": "c25e6802cbf27f3050f60b26b00c2d8dba1cb7fcdafe34c66006a7473b7b34d4", - "https://deno.land/std@0.224.0/fs/move.ts": "ca205d848908d7f217353bc5c623627b1333490b8b5d3ef4cab600a700c9bd8f", - "https://deno.land/std@0.224.0/fs/walk.ts": "cddf87d2705c0163bff5d7767291f05b0f46ba10b8b28f227c3849cace08d303", - "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", - "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", - "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e", - "https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", - "https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", - "https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c", - "https://deno.land/std@0.224.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", - "https://deno.land/std@0.224.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", - "https://deno.land/std@0.224.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", - "https://deno.land/std@0.224.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", - "https://deno.land/std@0.224.0/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d", - "https://deno.land/std@0.224.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", - "https://deno.land/std@0.224.0/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3", - "https://deno.land/std@0.224.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", - "https://deno.land/std@0.224.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", - "https://deno.land/std@0.224.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", - "https://deno.land/std@0.224.0/path/_interface.ts": "8dfeb930ca4a772c458a8c7bbe1e33216fe91c253411338ad80c5b6fa93ddba0", - "https://deno.land/std@0.224.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", - "https://deno.land/std@0.224.0/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e", - "https://deno.land/std@0.224.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643", - "https://deno.land/std@0.224.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", - "https://deno.land/std@0.224.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", - "https://deno.land/std@0.224.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", - "https://deno.land/std@0.224.0/path/format.ts": "6ce1779b0980296cf2bc20d66436b12792102b831fd281ab9eb08fa8a3e6f6ac", - "https://deno.land/std@0.224.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", - "https://deno.land/std@0.224.0/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972", - "https://deno.land/std@0.224.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", - "https://deno.land/std@0.224.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", - "https://deno.land/std@0.224.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", - "https://deno.land/std@0.224.0/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0", - "https://deno.land/std@0.224.0/path/mod.ts": "f6bd79cb08be0e604201bc9de41ac9248582699d1b2ee0ab6bc9190d472cf9cd", - "https://deno.land/std@0.224.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", - "https://deno.land/std@0.224.0/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f", - "https://deno.land/std@0.224.0/path/parse.ts": "77ad91dcb235a66c6f504df83087ce2a5471e67d79c402014f6e847389108d5a", - "https://deno.land/std@0.224.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", - "https://deno.land/std@0.224.0/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0", - "https://deno.land/std@0.224.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", - "https://deno.land/std@0.224.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", - "https://deno.land/std@0.224.0/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00", - "https://deno.land/std@0.224.0/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2", - "https://deno.land/std@0.224.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", - "https://deno.land/std@0.224.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", - "https://deno.land/std@0.224.0/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f", - "https://deno.land/std@0.224.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", - "https://deno.land/std@0.224.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", - "https://deno.land/std@0.224.0/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63", - "https://deno.land/std@0.224.0/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", - "https://deno.land/std@0.224.0/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", - "https://deno.land/std@0.224.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", - "https://deno.land/std@0.224.0/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", - "https://deno.land/std@0.224.0/path/posix/parse.ts": "09dfad0cae530f93627202f28c1befa78ea6e751f92f478ca2cc3b56be2cbb6a", - "https://deno.land/std@0.224.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", - "https://deno.land/std@0.224.0/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf", - "https://deno.land/std@0.224.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", - "https://deno.land/std@0.224.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", - "https://deno.land/std@0.224.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", - "https://deno.land/std@0.224.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", - "https://deno.land/std@0.224.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", - "https://deno.land/std@0.224.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", - "https://deno.land/std@0.224.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", - "https://deno.land/std@0.224.0/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660", - "https://deno.land/std@0.224.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", - "https://deno.land/std@0.224.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", - "https://deno.land/std@0.224.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", - "https://deno.land/std@0.224.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", - "https://deno.land/std@0.224.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", - "https://deno.land/std@0.224.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", - "https://deno.land/std@0.224.0/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8", - "https://deno.land/std@0.224.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", - "https://deno.land/std@0.224.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", - "https://deno.land/std@0.224.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", - "https://deno.land/std@0.224.0/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", - "https://deno.land/std@0.224.0/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", - "https://deno.land/std@0.224.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", - "https://deno.land/std@0.224.0/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", - "https://deno.land/std@0.224.0/path/windows/parse.ts": "08804327b0484d18ab4d6781742bf374976de662f8642e62a67e93346e759707", - "https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", - "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", - "https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", - "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", - "https://deno.land/std@0.224.0/yaml/_dumper/dumper.ts": "08b595b40841a2e1c75303f5096392323b6baf8e9662430a91e3b36fbe175fe9", - "https://deno.land/std@0.224.0/yaml/_dumper/dumper_state.ts": "9e29f700ea876ed230b43f11fa006fcb1a62eedc1e27d32baaeaf3210f19f1e7", - "https://deno.land/std@0.224.0/yaml/_error.ts": "f38cdebdb69cde16903d9aa2f3b8a3dd9d13e5f7f3570bf662bfaca69fef669e", - "https://deno.land/std@0.224.0/yaml/_loader/loader.ts": "bf9e8a99770b59bc887b43ebccea108cbe9146ae32d91f7ce558d62c946d3fe3", - "https://deno.land/std@0.224.0/yaml/_loader/loader_state.ts": "ee216de6040551940b85473c3185fdb7a6f3030b77153f87a6b7f63f82e489ea", - "https://deno.land/std@0.224.0/yaml/_mark.ts": "61097a614857fcebf7b2ecad057916d74c90cd160117a33c9e74bac60457410a", - "https://deno.land/std@0.224.0/yaml/_state.ts": "f3b1c1fd11860302f1f33e35e9ce089bf069d4943e8d67516cd6bedbba058c13", - "https://deno.land/std@0.224.0/yaml/_type/binary.ts": "f1a6e1d83dcc52b21cc3639cd98be44051cfc54065cc4f2a42065bce07ebc07d", - "https://deno.land/std@0.224.0/yaml/_type/bool.ts": "121743b23ba82a27ad6a3ec6298c7f5b0908f90e52707f8644a91f7ad51ed2ef", - "https://deno.land/std@0.224.0/yaml/_type/float.ts": "c5ed84b0aec1ec5dc05f6abfaaff672e8890d4d44a42120b4445c9754fca4eba", - "https://deno.land/std@0.224.0/yaml/_type/function.ts": "bbf705058942bf3370604b37eb77a10aadd72f986c237c9f69b43378a42202c1", - "https://deno.land/std@0.224.0/yaml/_type/int.ts": "c2dc88438a60fccc8d2226042bd18b9967753adaf6bd145feb8b99d567e432ce", - "https://deno.land/std@0.224.0/yaml/_type/map.ts": "ae2acb1cb837fb8e96c75c98611cfd45af847d0114ab5336333c318e7d4b12f4", - "https://deno.land/std@0.224.0/yaml/_type/merge.ts": "ad0d971f91d2fb9f4ab3eba0c837eae357b1804d6b798adc99dc917bc5306b11", - "https://deno.land/std@0.224.0/yaml/_type/mod.ts": "e8929d7b1c969a74f76338d4eb380ef8c4a26cd6441117d521f076b766e9c265", - "https://deno.land/std@0.224.0/yaml/_type/nil.ts": "cbe4387d02d5933322c21b25d8955c5e6228c492e391a6fb82dcf4f498cc421c", - "https://deno.land/std@0.224.0/yaml/_type/omap.ts": "cda915105ab22ba9e1d6317adacee8eec2d8ddaf864cc2f814e3e476946e72c6", - "https://deno.land/std@0.224.0/yaml/_type/pairs.ts": "dd39bb44c1b9abaf6172c63f73350475933151f07e05253b81f7860c9b507177", - "https://deno.land/std@0.224.0/yaml/_type/regexp.ts": "e49eb9e1c9356fd142bc15f7f323820d411fcc537b5ba3896df9a8b812d270a4", - "https://deno.land/std@0.224.0/yaml/_type/seq.ts": "2deffc7f970869bc01a1541b4961d076329a1c2b30b95e07918f3132db7c3fe2", - "https://deno.land/std@0.224.0/yaml/_type/set.ts": "be8a9e7237a7ffc92dfbe7f5e552d84b7eeba60f3f73cc77fc3c59d3506c74ea", - "https://deno.land/std@0.224.0/yaml/_type/str.ts": "88f0a1ba12295520cd57e96cd78d53aa0787d53c7a1c506155f418c496c2f550", - "https://deno.land/std@0.224.0/yaml/_type/timestamp.ts": "277a41a40fb93c3b2b3f5c373bf11b0b7856cc6a7b919e8ea130755e4029edc5", - "https://deno.land/std@0.224.0/yaml/_type/undefined.ts": "9d215953c65740f1764e0bdca021007573473f0c49e087f00d9ff02817ecfc97", - "https://deno.land/std@0.224.0/yaml/_utils.ts": "91bbe28b5e7000b9594e40ff5353f8fe7a7ba914eec917e1202cbaf5ac931c58", - "https://deno.land/std@0.224.0/yaml/mod.ts": "54e9bfad77c8cd58f49b65f4d568045ff08989ed36318a2ca733a43cb6f1bc00", - "https://deno.land/std@0.224.0/yaml/parse.ts": "f45278d9ebccb789af4eceeffa5c291e194bcf1fa9aab1b34ff52c2bd4a9d886", - "https://deno.land/std@0.224.0/yaml/schema.ts": "a0f7956d997852b5d1c6564bd73eb7352175cfba439707ac819b65b5a2ec173a", - "https://deno.land/std@0.224.0/yaml/schema/core.ts": "0a37c07710e3df4eb4edc02f4edf623bf8df5af72b34d8a7c0229d0bac2a7043", - "https://deno.land/std@0.224.0/yaml/schema/default.ts": "1367fd30420c7071ecc67e5b470838474e8259aaf64460f314af4b6bd8da497c", - "https://deno.land/std@0.224.0/yaml/schema/extended.ts": "248180c22697f37ed173057eae62ce4879865bb59f30c4908d698bed5edcc7c5", - "https://deno.land/std@0.224.0/yaml/schema/failsafe.ts": "0ac1cae5b86d8fe2c83ad0a17f8adc33106a452b7139f84e4b0bfaee2206730e", - "https://deno.land/std@0.224.0/yaml/schema/json.ts": "a0228a0c0bad7dece17ab848774fcadc2ccb5e51775c2d58d21d486917ba3ba1", - "https://deno.land/std@0.224.0/yaml/schema/mod.ts": "0e1558a4823834f106675e48ddc15338e04f6f18469d1a7d6b3f0e1ab06abcb2", - "https://deno.land/std@0.224.0/yaml/stringify.ts": "f0ed4e419cb40c807cf79ae4039d6cdf492be9a947121fff4d4b7cd1d4738bae", - "https://deno.land/std@0.224.0/yaml/type.ts": "708dde5f20b01cc1096489b7155b6af79a217d585afb841128e78c3c2391eb5c" - }, - "workspace": { - "dependencies": [ - "jsr:@deno/dnt@~0.41.3", - "jsr:@std/encoding@^1.0.10", - "jsr:@std/fs@^1.0.21", - "jsr:@std/io@~0.224.9", - "jsr:@std/log@~0.224.14", - "jsr:@std/net@^1.0.6", - "jsr:@std/path@^1.1.4", - "jsr:@std/streams@^1.0.16", - "jsr:@std/yaml@^1.0.10", - "jsr:@windmill-labs/cliffy-ansi@^1.0.0-rc.5", - "jsr:@windmill-labs/cliffy-command@^1.0.0-rc.5", - "jsr:@windmill-labs/cliffy-prompt@^1.0.0-rc.6", - "jsr:@windmill-labs/cliffy-table@^1.0.0-rc.5", - "npm:@types/diff@^5.2.3", - "npm:ws@8.18.0" - ] - } -} diff --git a/cli/deps.ts b/cli/deps.ts deleted file mode 100644 index 51e2d29b54..0000000000 --- a/cli/deps.ts +++ /dev/null @@ -1,83 +0,0 @@ -// cliffy -export { Command } from "jsr:@windmill-labs/cliffy-command@1.0.0-rc.5"; -export { Table } from "jsr:@windmill-labs/cliffy-table@1.0.0-rc.5"; -export { colors } from "jsr:@windmill-labs/cliffy-ansi@1.0.0-rc.5/colors"; -export { Secret } from "jsr:@windmill-labs/cliffy-prompt@1.0.0-rc.6/secret"; -export { Select } from "jsr:@windmill-labs/cliffy-prompt@1.0.0-rc.6/select"; -export { Confirm } from "jsr:@windmill-labs/cliffy-prompt@1.0.0-rc.6/confirm"; -export { Input } from "jsr:@windmill-labs/cliffy-prompt@1.0.0-rc.6/input"; -export { UpgradeCommand } from "jsr:@windmill-labs/cliffy-command@1.0.0-rc.5/upgrade"; -export { NpmProvider } from "jsr:@windmill-labs/cliffy-command@1.0.0-rc.5/upgrade/provider/npm"; -export { Provider } from "jsr:@windmill-labs/cliffy-command@1.0.0-rc.5/upgrade"; - -export { CompletionsCommand } from "jsr:@windmill-labs/cliffy-command@1.0.0-rc.5/completions"; -// std -export { ensureDir } from "jsr:@std/fs"; -export { SEPARATOR as SEP } from "jsr:@std/path"; -export * as path from "jsr:@std/path"; -export { encodeHex } from "jsr:@std/encoding@1.0.4"; -export { writeAllSync } from "jsr:@std/io/write-all"; -export { copy } from "jsr:@std/io/copy"; -export { readAll } from "jsr:@std/io/read-all"; - -export * as log from "jsr:@std/log"; -export { stringify as yamlStringify } from "jsr:@std/yaml"; - -import { parse as yamlParse, ParseOptions } from "jsr:@std/yaml"; - -export async function yamlParseFile(path: string, options: ParseOptions = {}) { - try { - return yamlParse(await Deno.readTextFile(path), options); - } catch (e) { - throw new Error(`Error parsing yaml ${path}`, { cause: e }); - } -} - -export function yamlParseContent( - path: string, - content: string, - options: ParseOptions = {}, -) { - try { - return yamlParse(content, options); - } catch (e) { - throw new Error(`Error parsing yaml ${path}`, { cause: e }); - } -} - -// other - -export * as Diff from "npm:diff"; -export { minimatch } from "npm:minimatch"; -export { default as JSZip } from "npm:jszip@3.8.0"; - -export * as express from "npm:express"; -export * as http from "node:http"; -export { WebSocket, WebSocketServer } from "npm:ws"; -export * as getPort from "npm:get-port@7.1.0"; -export * as open from "npm:open"; -export * as esMain from "npm:es-main"; -export * as windmillUtils from "jsr:@windmill-labs/shared-utils@1.0.12"; - -// needed for dnt transform -import * as wsTypes from "npm:@types/ws"; - -import { OpenAPI } from "./gen/index.ts"; - -export function setClient(token?: string, baseUrl?: string) { - if (baseUrl === undefined) { - baseUrl = getEnv("BASE_INTERNAL_URL") ?? - getEnv("BASE_URL") ?? - "http://localhost:8000"; - } - if (token === undefined) { - token = getEnv("WM_TOKEN") ?? "no_token"; - } - OpenAPI.WITH_CREDENTIALS = true; - OpenAPI.TOKEN = token; - OpenAPI.BASE = baseUrl + "/api"; -} - -const getEnv = (key: string) => { - return Deno.env.get(key); -}; diff --git a/cli/dnt.ts b/cli/dnt.ts deleted file mode 100644 index dd4ce1110e..0000000000 --- a/cli/dnt.ts +++ /dev/null @@ -1,87 +0,0 @@ -// ex. scripts/build_npm.ts -import { build, emptyDir } from "jsr:@deno/dnt@0.42.3"; -import { VERSION } from "./src/main.ts"; -await emptyDir("./npm"); - -await build({ - entryPoints: [ - "src/main.ts", - { - kind: "bin", - name: "wmill", // command name - path: "./src/main.ts", - }, - ], - outDir: "./npm", - test: false, // Disable all tests in npm build since they use Deno-specific APIs - shims: { - // see JS docs for overview and more options - deno: true, - // shims to only use in the tests - customDev: [{ - // this is what `timers: "dev"` does internally - package: { - name: "@deno/shim-timers", - version: "~0.1.0", - }, - globalNames: ["setTimeout", "setInterval"], - }], - }, - scriptModule: false, - filterDiagnostic(diagnostic) { - if ( - diagnostic.file?.fileName.includes("node_modules/") || - diagnostic.file?.fileName.includes("src/deps/") || - diagnostic.file?.fileName.includes("src/deps.ts") || - diagnostic.file?.fileName.includes("src/utils/utils.ts") - ) { - return false; // ignore all diagnostics in this file - } - // console.log(diagnostic.file?.fileName); - return true; - }, - declaration: "separate", - package: { - // package.json properties - name: "windmill-cli", - version: VERSION, - description: "CLI for Windmill", - license: "Apache 2.0", - main: "esm/main.js", - repository: { - type: "git", - url: "git+https://github.com/windmill-labs/windmill.git", - }, - bugs: { - url: "https://github.com/windmill-labs/windmill/issues", - }, - }, - - postBuild() { - // steps to run after building and before running the tests - // add shebang to npm/esm/main.js - const dirs = [ - "nu", - "ts", - "regex", - "py", - "go", - "php", - "rust", - "yaml", - "csharp", - "java", - "ruby", - // for related places search: ADD_NEW_LANG - ]; - - for (const l of dirs) { - Deno.copyFileSync( - "wasm/" + l + "/windmill_parser_wasm_bg.wasm", - "npm/esm/wasm/" + l + "/windmill_parser_wasm_bg.wasm" - ); - } - Deno.copyFileSync("../LICENSE", "npm/LICENSE"); - Deno.copyFileSync("README.md", "npm/README.md"); - }, -}); diff --git a/cli/gen_wm_client.sh b/cli/gen_wm_client.sh index f6e5a5e094..af64fe5e59 100755 --- a/cli/gen_wm_client.sh +++ b/cli/gen_wm_client.sh @@ -6,8 +6,8 @@ rm -rf "${script_dirpath}/gen" npx --yes @hey-api/openapi-ts@0.53.1 --input "${script_dirpath}/../backend/windmill-api/openapi.yaml" --output "${script_dirpath}/gen" --useOptions --client legacy/fetch --schemas false cat < temp_file && mv temp_file gen/core/OpenAPI.ts -const getEnv = (key: string) => { - return Deno.env.get(key) +const getEnv = (key: string): string | undefined => { + return process.env[key] }; const baseUrl = getEnv("BASE_INTERNAL_URL") ?? getEnv("BASE_URL") ?? "http://localhost:8000"; diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 0000000000..2c8df16d77 --- /dev/null +++ b/cli/package.json @@ -0,0 +1,54 @@ +{ + "name": "wmill-dev", + "private": true, + "type": "module", + "bin": { + "wmill": "src/main.ts" + }, + "scripts": { + "dev": "bun run src/main.ts", + "build": "./build.sh", + "test": "bun test test/", + "check": "bunx tsc --noEmit", + "gen-client": "./gen_wm_client.sh && ./windmill-utils-internal/gen_wm_client.sh" + }, + "dependencies": { + "@ayonli/jsext": "^1.9.0", + "@cliffy/ansi": "npm:@jsr/cliffy__ansi@1.0.0", + "@cliffy/command": "npm:@jsr/cliffy__command@1.0.0", + "@cliffy/prompt": "npm:@jsr/cliffy__prompt@1.0.0", + "@cliffy/table": "npm:@jsr/cliffy__table@1.0.0", + "@std/encoding": "npm:@jsr/std__encoding@1.0.10", + "@std/log": "npm:@jsr/std__log@0.224.14", + "@std/path": "npm:@jsr/std__path@1.1.4", + "@std/yaml": "npm:@jsr/std__yaml@1.0.10", + "@windmill-labs/shared-utils": "npm:@jsr/windmill-labs__shared-utils@1.0.12", + "diff": "^5.2.0", + "esbuild": "0.24.2", + "svelte": "^5.45.2", + "get-port": "7.1.0", + "jszip": "3.8.0", + "minimatch": "^10.0.0", + "open": "^10.0.0", + "windmill-parser-wasm-csharp": "*", + "windmill-parser-wasm-go": "*", + "windmill-parser-wasm-java": "*", + "windmill-parser-wasm-nu": "*", + "windmill-parser-wasm-php": "*", + "windmill-parser-wasm-py": "*", + "windmill-parser-wasm-regex": "*", + "windmill-parser-wasm-ruby": "*", + "windmill-parser-wasm-rust": "*", + "windmill-parser-wasm-ts": "*", + "windmill-parser-wasm-yaml": "*", + "windmill-yaml-validator": "1.1.1", + "ws": "8.18.0", + "yaml": "^2.7.0" + }, + "devDependencies": { + "@types/diff": "^5.2.3", + "@types/ws": "^8.5.0", + "@types/node": "^22.0.0", + "typescript": "^5.7.0" + } +} diff --git a/cli/src/commands/app/app.ts b/cli/src/commands/app/app.ts index 8eedc2a2c2..febd55e918 100644 --- a/cli/src/commands/app/app.ts +++ b/cli/src/commands/app/app.ts @@ -1,15 +1,12 @@ -// deno-lint-ignore-file no-explicit-any import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace, validatePath } from "../../core/context.ts"; -import { - colors, - Command, - log, - SEP, - Table, - windmillUtils, - yamlParseFile, -} from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import { Table } from "@cliffy/table"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; +import * as windmillUtils from "@windmill-labs/shared-utils"; +import { yamlParseFile } from "../../utils/yaml.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { ListableApp, Policy } from "../../../gen/types.gen.ts"; diff --git a/cli/src/commands/app/app_metadata.ts b/cli/src/commands/app/app_metadata.ts index 51c8a97ffd..be0decfa32 100644 --- a/cli/src/commands/app/app_metadata.ts +++ b/cli/src/commands/app/app_metadata.ts @@ -1,12 +1,10 @@ -// deno-lint-ignore-file no-explicit-any import path from "node:path"; -import { - SEP, - colors, - log, - yamlParseFile, - yamlStringify, -} from "../../../deps.ts"; +import { readFile, mkdir } from "node:fs/promises"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; +import { yamlParseFile } from "../../utils/yaml.ts"; +import { stringify as yamlStringify } from "@std/yaml"; import { GlobalOptions } from "../../types.ts"; import { checkifMetadataUptodate, @@ -86,7 +84,7 @@ async function generateAppHash( } } catch (error: any) { // If runnables folder doesn't exist, that's okay - if (error.name !== "NotFound") { + if (error.code !== "ENOENT") { throw error; } } @@ -351,7 +349,7 @@ async function updateRawAppRunnables( // Ensure runnables folder exists try { - await Deno.mkdir(runnablesFolder, { recursive: true }); + await mkdir(runnablesFolder, { recursive: true }); } catch { // Folder may already exist } @@ -736,7 +734,7 @@ export async function inferRunnableSchemaFromFile( ); let content: string; try { - content = await Deno.readTextFile(fullFilePath); + content = await readFile(fullFilePath, "utf-8"); } catch { log.warn(colors.yellow(`Could not read file: ${fullFilePath}`)); return undefined; @@ -786,7 +784,7 @@ export async function generateLocksCommand( const { generateAppLocksInternal } = await import("./app_metadata.ts"); const { elementsToMap, FSFSElement } = await import("../sync/sync.ts"); const { ignoreF } = await import("../sync/sync.ts"); - const { Confirm } = await import("../../../deps.ts"); + const { Confirm } = await import("@cliffy/prompt/confirm"); if (appPath == "") { appPath = undefined; @@ -813,7 +811,7 @@ export async function generateLocksCommand( // Generate metadata for all apps const ignore = await ignoreF(opts); const elems = await elementsToMap( - await FSFSElement(Deno.cwd(), [], true), + await FSFSElement(process.cwd(), [], true), (p, isD) => { return ( ignore(p, isD) || diff --git a/cli/src/commands/app/bundle.ts b/cli/src/commands/app/bundle.ts index 3b8ebc03c5..d610d743f2 100644 --- a/cli/src/commands/app/bundle.ts +++ b/cli/src/commands/app/bundle.ts @@ -1,10 +1,10 @@ -// deno-lint-ignore-file no-explicit-any import * as fs from "node:fs"; import * as path from "node:path"; import process from "node:process"; import { spawn } from "node:child_process"; -import { log, colors } from "../../../deps.ts"; -import { windmillUtils } from "../../../deps.ts"; +import * as log from "@std/log"; +import { colors } from "@cliffy/ansi/colors"; +import * as windmillUtils from "@windmill-labs/shared-utils"; export interface BundleOptions { entryPoint?: string; outDir?: string; @@ -66,7 +66,7 @@ function createSveltePlugin(appDir: string): any { setup(build: any) { build.onLoad({ filter: /\.svelte$/ }, async (args: any) => { // Import svelte compiler from the project's node_modules - const svelte = await import("npm:svelte@5.45.2/compiler"); + const svelte = await import("svelte/compiler"); // Load the file from the file system const source = await fs.promises.readFile(args.path, "utf8"); @@ -118,7 +118,7 @@ export async function createFrameworkPlugins(appDir: string): Promise { log.info(colors.blue("🔧 Vue detected, adding vue plugin...")); throw new Error("Vue plugin not supported yet"); // try { - // const esbuildPluginVue = await import("npm:esbuild-plugin-vue3@0.5.1"); + // const esbuildPluginVue = await import("esbuild-plugin-vue3"); // plugins.push(esbuildPluginVue.default()); // } catch (error: any) { // log.warn(colors.yellow(`Failed to load vue plugin: ${error.message}`)); @@ -164,7 +164,7 @@ export async function createBundle( options: BundleOptions = {} ): Promise { // Dynamically import esbuild - const esbuild = await import("npm:esbuild@0.24.2"); + const esbuild = await import("esbuild"); // Detect frameworks to determine default entry point const frameworks = detectFrameworks(process.cwd()); diff --git a/cli/src/commands/app/dev.ts b/cli/src/commands/app/dev.ts index 76f3930fa5..7cc106baa4 100644 --- a/cli/src/commands/app/dev.ts +++ b/cli/src/commands/app/dev.ts @@ -1,14 +1,11 @@ -// deno-lint-ignore-file no-explicit-any -import { - colors, - Command, - getPort, - log, - open, - SEP, - windmillUtils, - yamlParseFile, -} from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; +import * as windmillUtils from "@windmill-labs/shared-utils"; +import { yamlParseFile } from "../../utils/yaml.ts"; +import * as getPort from "get-port"; +import * as open from "open"; import { GlobalOptions } from "../../types.ts"; import * as http from "node:http"; import * as fs from "node:fs"; @@ -16,7 +13,8 @@ import * as path from "node:path"; import process from "node:process"; import { Buffer } from "node:buffer"; import { writeFileSync } from "node:fs"; -import { WebSocket, WebSocketServer } from "npm:ws"; +import { readFile } from "node:fs/promises"; +import { WebSocket, WebSocketServer } from "ws"; import { createFrameworkPlugins, detectFrameworks, @@ -336,7 +334,7 @@ async function dev(opts: DevOptions, appFolder?: string) { if (!fs.existsSync(targetDir)) { log.error(colors.red(`Error: Directory not found: ${targetDir}`)); - Deno.exit(1); + process.exit(1); } } @@ -355,7 +353,7 @@ async function dev(opts: DevOptions, appFolder?: string) { }' or specify one as argument.`, ), ); - Deno.exit(1); + process.exit(1); } // Check for raw_app.yaml in target directory @@ -369,7 +367,7 @@ async function dev(opts: DevOptions, appFolder?: string) { } folder containing a raw_app.yaml file.`, ), ); - Deno.exit(1); + process.exit(1); } // Resolve workspace and authenticate (from original cwd to find wmill.yaml) @@ -387,7 +385,7 @@ async function dev(opts: DevOptions, appFolder?: string) { const appPath = rawApp?.custom_path ?? "u/unknown/newapp"; // Dynamically import esbuild only when the dev command is called - const esbuild = await import("npm:esbuild@0.24.2"); + const esbuild = await import("esbuild"); const port = opts.port ?? (await getPort.default({ @@ -410,7 +408,7 @@ async function dev(opts: DevOptions, appFolder?: string) { `Entry point "${entryPoint}" not found. Please specify a valid entry point with --entry.`, ), ); - Deno.exit(1); + process.exit(1); } // Ensure node_modules exists @@ -525,99 +523,85 @@ async function dev(opts: DevOptions, appFolder?: string) { // Watch runnables folder for changes const runnablesPath = path.join(process.cwd(), APP_BACKEND_FOLDER); - let runnablesWatcher: Deno.FsWatcher | undefined; + let runnablesWatcher: fs.FSWatcher | undefined; if (fs.existsSync(runnablesPath)) { log.info( colors.blue(`👁️ Watching runnables folder at: ${runnablesPath}\n`), ); - runnablesWatcher = Deno.watchFs(runnablesPath); + runnablesWatcher = fs.watch(runnablesPath, { recursive: true }); // Per-file debounce timeouts for schema inference (longer debounce for typing) const schemaInferenceTimeouts: Record> = {}; const SCHEMA_DEBOUNCE_MS = 500; // Wait 500ms after last change before inferring schema - // Handle runnables file changes in the background - (async () => { - try { - for await (const event of runnablesWatcher!) { - // Process each changed path with individual debouncing - for (const changedPath of event.paths) { - const relativePath = path.relative(process.cwd(), changedPath); - const relativeToRunnables = path.relative( - runnablesPath, - changedPath, - ); + // Handle runnables file changes via callback + runnablesWatcher.on("change", (_eventType, filename) => { + if (!filename) return; + const fileStr = typeof filename === "string" ? filename : filename.toString(); + const changedPath = path.join(runnablesPath, fileStr); + const relativePath = path.relative(process.cwd(), changedPath); + const relativeToRunnables = fileStr; - // Skip non-modify events for schema inference - if (event.kind !== "modify" && event.kind !== "create") { - continue; - } + // Skip lock files + if (changedPath.endsWith(".lock")) { + return; + } - // Skip lock files - if (changedPath.endsWith(".lock")) { - continue; - } + // Log the change event + log.info( + colors.cyan( + `📝 Runnable changed [${_eventType}]: ${relativePath}`, + ), + ); - // Log the change event + // Debounce schema inference per file (wait for typing to finish) + if (schemaInferenceTimeouts[changedPath]) { + clearTimeout(schemaInferenceTimeouts[changedPath]); + } + + schemaInferenceTimeouts[changedPath] = setTimeout(async () => { + delete schemaInferenceTimeouts[changedPath]; + + try { + log.info( + colors.cyan( + `📝 Inferring schema for: ${relativeToRunnables}`, + ), + ); + // Infer schema for this runnable (returns schema in memory, doesn't write to file) + const result = await inferRunnableSchemaFromFile( + process.cwd(), + relativeToRunnables, + ); + if (result) { + // Store inferred schema in memory + inferredSchemas[result.runnableId] = result.schema; log.info( - colors.cyan( - `📝 Runnable changed [${event.kind}]: ${relativePath}`, + colors.green( + ` Inferred Schemas: ${ + JSON.stringify( + inferredSchemas, + null, + 2, + ) + }`, ), ); - - // Debounce schema inference per file (wait for typing to finish) - if (schemaInferenceTimeouts[changedPath]) { - clearTimeout(schemaInferenceTimeouts[changedPath]); - } - - schemaInferenceTimeouts[changedPath] = setTimeout(async () => { - delete schemaInferenceTimeouts[changedPath]; - - try { - log.info( - colors.cyan( - `📝 Inferring schema for: ${relativeToRunnables}`, - ), - ); - // Infer schema for this runnable (returns schema in memory, doesn't write to file) - const result = await inferRunnableSchemaFromFile( - process.cwd(), - relativeToRunnables, - ); - if (result) { - // log.info(colors.green(` Schema: ${JSON.stringify(result.schema, null, 2)}`)); - // log.info(colors.green(` Runnable ID: ${result.runnableId}`)); - // Store inferred schema in memory - inferredSchemas[result.runnableId] = result.schema; - log.info( - colors.green( - ` Inferred Schemas: ${ - JSON.stringify( - inferredSchemas, - null, - 2, - ) - }`, - ), - ); - // Regenerate wmill.d.ts with updated schema from memory - await genRunnablesTs(inferredSchemas); - } - } catch (error: any) { - log.error( - colors.red(`Error inferring schema: ${error.message}`), - ); - } - }, SCHEMA_DEBOUNCE_MS); + // Regenerate wmill.d.ts with updated schema from memory + await genRunnablesTs(inferredSchemas); } + } catch (error: any) { + log.error( + colors.red(`Error inferring schema: ${error.message}`), + ); } - } catch (error: any) { - if (error.name !== "Interrupted") { - log.error(colors.red(`Error watching runnables: ${error.message}`)); - } - } - })(); + }, SCHEMA_DEBOUNCE_MS); + }); + + runnablesWatcher.on("error", (error: Error) => { + log.error(colors.red(`Error watching runnables: ${error.message}`)); + }); } else { log.info( colors.gray( @@ -781,7 +765,7 @@ async function dev(opts: DevOptions, appFolder?: string) { const fileName = path.basename(filePath); try { - const sqlContent = await Deno.readTextFile(filePath); + const sqlContent = await readFile(filePath, "utf-8"); if (!sqlContent.trim()) { log.info(colors.gray(`Skipping empty file: ${fileName}`)); @@ -837,7 +821,7 @@ async function dev(opts: DevOptions, appFolder?: string) { // If there's a current SQL file being shown, send it to the new client if (currentSqlFile && fs.existsSync(currentSqlFile)) { try { - const sqlContent = await Deno.readTextFile(currentSqlFile); + const sqlContent = await readFile(currentSqlFile, "utf-8"); const datatable = await getDatatableConfig(); const fileName = path.basename(currentSqlFile); @@ -1164,7 +1148,7 @@ async function dev(opts: DevOptions, appFolder?: string) { }); // Watch sql_to_apply folder for SQL migration files - let sqlWatcher: Deno.FsWatcher | undefined; + let sqlWatcher: fs.FSWatcher | undefined; // Helper to scan for existing SQL files and add them to the queue async function scanExistingSqlFiles(): Promise { @@ -1207,53 +1191,46 @@ async function dev(opts: DevOptions, appFolder?: string) { log.info( colors.blue(`🗃️ Watching sql_to_apply folder at: ${sqlToApplyPath}\n`), ); - sqlWatcher = Deno.watchFs(sqlToApplyPath); + sqlWatcher = fs.watch(sqlToApplyPath, { recursive: true }); // Debounce timeout for SQL file changes const sqlDebounceTimeouts: Record> = {}; const SQL_DEBOUNCE_MS = 300; - // Handle SQL file changes in the background - (async () => { - try { - for await (const event of sqlWatcher!) { - for (const changedPath of event.paths) { - // Only handle .sql files - if (!changedPath.endsWith(".sql")) { - continue; - } + // Handle SQL file changes via callback + sqlWatcher.on("change", (_eventType, filename) => { + if (!filename) return; + const fileStr = typeof filename === "string" ? filename : filename.toString(); + const changedPath = path.join(sqlToApplyPath, fileStr); - // Only handle modify and create events - if (event.kind !== "modify" && event.kind !== "create") { - continue; - } - - const fileName = path.basename(changedPath); - - // Debounce per file - if (sqlDebounceTimeouts[changedPath]) { - clearTimeout(sqlDebounceTimeouts[changedPath]); - } - - sqlDebounceTimeouts[changedPath] = setTimeout(async () => { - delete sqlDebounceTimeouts[changedPath]; - - log.info(colors.cyan(`📋 SQL file detected: ${fileName}`)); - - // Add to queue and process - queueSqlFile(changedPath); - await processNextSqlFile(); - }, SQL_DEBOUNCE_MS); - } - } - } catch (error: any) { - if (error.name !== "Interrupted") { - log.error( - colors.red(`Error watching sql_to_apply: ${error.message}`), - ); - } + // Only handle .sql files + if (!changedPath.endsWith(".sql")) { + return; } - })(); + + const fileName = path.basename(changedPath); + + // Debounce per file + if (sqlDebounceTimeouts[changedPath]) { + clearTimeout(sqlDebounceTimeouts[changedPath]); + } + + sqlDebounceTimeouts[changedPath] = setTimeout(async () => { + delete sqlDebounceTimeouts[changedPath]; + + log.info(colors.cyan(`📋 SQL file detected: ${fileName}`)); + + // Add to queue and process + queueSqlFile(changedPath); + await processNextSqlFile(); + }, SQL_DEBOUNCE_MS); + }); + + sqlWatcher.on("error", (error: Error) => { + log.error( + colors.red(`Error watching sql_to_apply: ${error.message}`), + ); + }); // Scan for existing SQL files after a delay (to let WebSocket clients connect) setTimeout(() => { diff --git a/cli/src/commands/app/generate_agents.ts b/cli/src/commands/app/generate_agents.ts index d2811e0b07..f8e34a34d9 100644 --- a/cli/src/commands/app/generate_agents.ts +++ b/cli/src/commands/app/generate_agents.ts @@ -1,12 +1,18 @@ -import { colors, Command, log, yamlParseFile } from "../../../deps.ts"; +import * as fs from "node:fs"; +import { writeFile } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; + +import { Command } from "@cliffy/command"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { yamlParseFile } from "../../utils/yaml.ts"; import { GlobalOptions } from "../../types.ts"; import { resolveWorkspace } from "../../core/context.ts"; import { requireLogin } from "../../core/auth.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { DataTableSchema } from "../../../gen/types.gen.ts"; import { generateAgentsDocumentation } from "../sync/sync.ts"; -import path from "node:path"; -import * as fs from "node:fs"; import { getFolderSuffix, hasFolderSuffix, @@ -192,14 +198,14 @@ export async function regenerateAgentDocs( // Generate and write AGENTS.md const agentsContent = generateAgentsDocumentation(localData); - await Deno.writeTextFile(path.join(targetDir, "AGENTS.md"), agentsContent); + await writeFile(path.join(targetDir, "AGENTS.md"), agentsContent, "utf-8"); // Generate and write CLAUDE.md referencing AGENTS.md - await Deno.writeTextFile(path.join(targetDir, "CLAUDE.md"), `Instructions are in @AGENTS.md\n`); + await writeFile(path.join(targetDir, "CLAUDE.md"), `Instructions are in @AGENTS.md\n`, "utf-8"); // Generate and write DATATABLES.md const datatablesContent = generateDatatablesMarkdown(schemas, localData); - await Deno.writeTextFile(path.join(targetDir, "DATATABLES.md"), datatablesContent); + await writeFile(path.join(targetDir, "DATATABLES.md"), datatablesContent, "utf-8"); if (!silent) { log.info(colors.green(`✓ Generated AGENTS.md, CLAUDE.md, and DATATABLES.md`)); @@ -229,7 +235,7 @@ async function generateAgents( appFolder?: string ) { // Resolve the app folder - const cwd = Deno.cwd(); + const cwd = process.cwd(); let targetDir = cwd; if (appFolder) { @@ -252,7 +258,7 @@ async function generateAgents( ) ); log.info(colors.gray("Usage: wmill app generate-agents [app_folder]")); - Deno.exit(1); + process.exit(1); } } @@ -262,7 +268,7 @@ async function generateAgents( log.error( colors.red(`Error: raw_app.yaml not found in ${targetDir}`) ); - Deno.exit(1); + process.exit(1); } // Resolve workspace and authenticate @@ -272,7 +278,6 @@ async function generateAgents( await regenerateAgentDocs(workspace.workspaceId, targetDir); } -// deno-lint-ignore no-explicit-any const command = new Command() .description("regenerate AGENTS.md and DATATABLES.md from remote workspace") .arguments("[app_folder:string]") diff --git a/cli/src/commands/app/lint.ts b/cli/src/commands/app/lint.ts index af8d7e5ab3..932f714185 100644 --- a/cli/src/commands/app/lint.ts +++ b/cli/src/commands/app/lint.ts @@ -1,8 +1,10 @@ -// deno-lint-ignore-file no-explicit-any import * as fs from "node:fs"; import * as path from "node:path"; import process from "node:process"; -import { colors, Command, log, yamlParseFile } from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { yamlParseFile } from "../../utils/yaml.ts"; import { GlobalOptions } from "../../types.ts"; import { createBundle } from "./bundle.ts"; import { APP_BACKEND_FOLDER } from "./app_metadata.ts"; @@ -224,7 +226,7 @@ async function lint(opts: LintOptions, appFolder?: string) { log.info(colors.red(` - ${error}`)); }); log.info(colors.red("\n❌ Lint failed\n")); - Deno.exit(1); + process.exit(1); } log.info(colors.green("\n✅ All checks passed\n")); diff --git a/cli/src/commands/app/new.ts b/cli/src/commands/app/new.ts index d79600118d..fbbc2b4c36 100644 --- a/cli/src/commands/app/new.ts +++ b/cli/src/commands/app/new.ts @@ -1,13 +1,11 @@ -import { - colors, - Command, - Confirm, - ensureDir, - Input, - log, - Select, - yamlStringify, -} from "../../../deps.ts"; +import { stat, writeFile, mkdir } from "node:fs/promises"; +import { Command } from "@cliffy/command"; +import { colors } from "@cliffy/ansi/colors"; +import { Confirm } from "@cliffy/prompt/confirm"; +import { Input } from "@cliffy/prompt/input"; +import { Select } from "@cliffy/prompt/select"; +import * as log from "@std/log"; +import { stringify as yamlStringify } from "@std/yaml"; import { GlobalOptions } from "../../types.ts"; import { generateAgentsDocumentation, generateDatatablesDocumentation, yamlOptions } from "../sync/sync.ts"; import { resolveWorkspace } from "../../core/context.ts"; @@ -480,11 +478,11 @@ CREATE SCHEMA IF NOT EXISTS ${schemaName}; // Create the directory structure - preserve full path (e.g., f/foobar/x/y becomes f/foobar/x/y.raw_app) const folderName = buildFolderPath(appPath, "raw_app"); - const appDir = path.join(Deno.cwd(), folderName); + const appDir = path.join(process.cwd(), folderName); // Check if directory already exists try { - await Deno.stat(appDir); + await stat(appDir); const overwrite = await Confirm.prompt({ message: `Directory '${folderName}' already exists. Overwrite?`, default: false, @@ -497,9 +495,9 @@ CREATE SCHEMA IF NOT EXISTS ${schemaName}; // Directory doesn't exist, which is good } - await ensureDir(appDir); - await ensureDir(path.join(appDir, "backend")); - await ensureDir(path.join(appDir, "sql_to_apply")); + await mkdir(appDir, { recursive: true }); + await mkdir(path.join(appDir, "backend"), { recursive: true }); + await mkdir(path.join(appDir, "sql_to_apply"), { recursive: true }); // Create raw_app.yaml with data configuration const rawAppConfig: Record = { @@ -511,15 +509,15 @@ CREATE SCHEMA IF NOT EXISTS ${schemaName}; rawAppConfig.data = dataConfig; } - await Deno.writeTextFile( + await writeFile( path.join(appDir, "raw_app.yaml"), - yamlStringify(rawAppConfig, yamlOptions) + yamlStringify(rawAppConfig, yamlOptions), "utf-8" ); // Create template files for (const [filePath, content] of Object.entries(template.files)) { const fullPath = path.join(appDir, filePath.slice(1)); // Remove leading slash - await Deno.writeTextFile(fullPath, content.trim() + "\n"); + await writeFile(fullPath, content.trim() + "\n", "utf-8"); } // Create AGENTS.md - main documentation for AI agents @@ -532,22 +530,22 @@ CREATE SCHEMA IF NOT EXISTS ${schemaName}; : undefined; const agentsContent = generateAgentsDocumentation(dataForDocs); - await Deno.writeTextFile( + await writeFile( path.join(appDir, "AGENTS.md"), - agentsContent + agentsContent, "utf-8" ); // Create CLAUDE.md referencing AGENTS.md - await Deno.writeTextFile( + await writeFile( path.join(appDir, "CLAUDE.md"), - `Instructions are in @AGENTS.md\n` + `Instructions are in @AGENTS.md\n`, "utf-8" ); // Create DATATABLES.md with the configured data const datatablesContent = generateDatatablesDocumentation(dataForDocs); - await Deno.writeTextFile( + await writeFile( path.join(appDir, "DATATABLES.md"), - datatablesContent + datatablesContent, "utf-8" ); // Create example backend runnable @@ -555,20 +553,20 @@ CREATE SCHEMA IF NOT EXISTS ${schemaName}; type: "inline", path: undefined, }; - await Deno.writeTextFile( + await writeFile( path.join(appDir, "backend", "a.yaml"), - yamlStringify(exampleRunnable, yamlOptions) + yamlStringify(exampleRunnable, yamlOptions), "utf-8" ); - await Deno.writeTextFile( + await writeFile( path.join(appDir, "backend", "a.ts"), `export async function main(x: number): Promise { return \`Hello from backend! x = \${x}\`; } -` +`, "utf-8" ); // Create sql_to_apply README - await Deno.writeTextFile( + await writeFile( path.join(appDir, "sql_to_apply", "README.md"), `# SQL Migrations Folder @@ -601,9 +599,9 @@ This folder is for SQL migration files that will be applied to datatables during // Create schema creation SQL file if a new schema was requested if (createSchemaSQL && schemaName) { - await Deno.writeTextFile( + await writeFile( path.join(appDir, "sql_to_apply", `000_create_schema_${schemaName}.sql`), - createSchemaSQL + createSchemaSQL, "utf-8" ); } @@ -666,7 +664,6 @@ This folder is for SQL migration files that will be applied to datatables during log.info(colors.gray(" 4. wmill sync push (to deploy when ready)")); } -// deno-lint-ignore no-explicit-any const command = new Command() .description("create a new raw app from a template") .action(newApp as any); diff --git a/cli/src/commands/app/raw_apps.ts b/cli/src/commands/app/raw_apps.ts index c6c408d5b2..5c8189284a 100644 --- a/cli/src/commands/app/raw_apps.ts +++ b/cli/src/commands/app/raw_apps.ts @@ -1,17 +1,15 @@ -// deno-lint-ignore-file no-explicit-any import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace, validatePath } from "../../core/context.ts"; -import { - colors, - log, - SEP, - windmillUtils, - yamlParseFile, - yamlStringify, -} from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; +import * as windmillUtils from "@windmill-labs/shared-utils"; +import { yamlParseFile } from "../../utils/yaml.ts"; +import { stringify as yamlStringify } from "@std/yaml"; import * as wmill from "../../../gen/services.gen.ts"; import { Policy } from "../../../gen/types.gen.ts"; import path from "node:path"; +import { readFile, readdir } from "node:fs/promises"; import { GlobalOptions, isSuperset } from "../../types.ts"; import { deepEqual } from "../../utils/utils.ts"; @@ -67,8 +65,8 @@ async function findRunnableContentFile( // Check if this is a recognized extension if (EXTENSION_TO_LANGUAGE[ext]) { try { - const content = await Deno.readTextFile( - path.join(backendPath, fileName), + const content = await readFile( + path.join(backendPath, fileName), "utf-8", ); return { ext, content }; } catch { @@ -130,8 +128,9 @@ export async function loadRunnablesFromBackend( try { // First, collect all files in the backend folder const allFiles: string[] = []; - for await (const entry of Deno.readDir(backendPath)) { - if (entry.isFile) { + const _entries = await readdir(backendPath, { withFileTypes: true }); + for (const entry of _entries) { + if (entry.isFile()) { allFiles.push(entry.name); } } @@ -165,8 +164,9 @@ export async function loadRunnablesFromBackend( // Try to load lock file let lock: string | undefined; try { - lock = await Deno.readTextFile( + lock = await readFile( path.join(backendPath, `${runnableId}.lock`), + "utf-8", ); } catch { // No lock file, that's fine @@ -226,8 +226,8 @@ export async function loadRunnablesFromBackend( // Try to load lock file let lock: string | undefined; try { - lock = await Deno.readTextFile( - path.join(backendPath, `${runnableId}.lock`), + lock = await readFile( + path.join(backendPath, `${runnableId}.lock`), "utf-8", ); } catch { // No lock file, that's fine @@ -245,7 +245,7 @@ export async function loadRunnablesFromBackend( } } } catch (error: any) { - if (error.name !== "NotFound") { + if (error.code !== "ENOENT") { throw error; } } @@ -291,11 +291,12 @@ async function collectAppFiles( const files: Record = {}; async function readDirRecursive(dir: string, basePath: string = "/") { - for await (const entry of Deno.readDir(dir)) { + const dirEntries = await readdir(dir, { withFileTypes: true }); + for (const entry of dirEntries) { const fullPath = dir + entry.name; const relativePath = basePath + entry.name; - if (entry.isDirectory) { + if (entry.isDirectory()) { // Skip the runnables, node_modules, and sql_to_apply subfolders if ( entry.name === APP_BACKEND_FOLDER || @@ -307,7 +308,7 @@ async function collectAppFiles( continue; } await readDirRecursive(fullPath + SEP, relativePath + "/"); - } else if (entry.isFile) { + } else if (entry.isFile()) { // Skip generated/metadata files that shouldn't be part of the app if ( entry.name === "raw_app.yaml" || @@ -318,7 +319,7 @@ async function collectAppFiles( ) { continue; } - const content = await Deno.readTextFile(fullPath); + const content = await readFile(fullPath, "utf-8"); files[relativePath] = content; } } diff --git a/cli/src/commands/dependencies/dependencies.ts b/cli/src/commands/dependencies/dependencies.ts index 9ca4a11e0d..aa556974df 100644 --- a/cli/src/commands/dependencies/dependencies.ts +++ b/cli/src/commands/dependencies/dependencies.ts @@ -1,8 +1,9 @@ -// deno-lint-ignore-file no-explicit-any import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace } from "../../core/context.ts"; import { GlobalOptions } from "../../types.ts"; -import { colors, Command, log } from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import * as log from "@std/log"; import * as wmill from "../../../gen/services.gen.ts"; import fs from "node:fs"; import { workspaceDependenciesPathToLanguageAndFilename } from "../../utils/metadata.ts"; diff --git a/cli/src/commands/dev/dev.ts b/cli/src/commands/dev/dev.ts index c9c3d79083..e0a47f2c69 100644 --- a/cli/src/commands/dev/dev.ts +++ b/cli/src/commands/dev/dev.ts @@ -1,15 +1,14 @@ -import { - Command, - SEP, - WebSocketServer, - express, - getPort, - http, - log, - open, - WebSocket, - yamlParseFile, -} from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; +import { yamlParseFile } from "../../utils/yaml.ts"; +import { WebSocket, WebSocketServer } from "ws"; + +import * as getPort from "get-port"; +import * as http from "node:http"; +import * as open from "open"; +import { readFile, realpath } from "node:fs/promises"; +import { watch } from "node:fs"; import { getTypeStrFromPath, GlobalOptions } from "../../types.ts"; import { ignoreF } from "../sync/sync.ts"; import { requireLogin } from "../../core/auth.ts"; @@ -40,25 +39,30 @@ async function dev(opts: GlobalOptions & SyncOptions) { const conf = await readConfigFile(); let currentLastEdit: LastEditScript | LastEditFlow | undefined = undefined; - const watcher = Deno.watchFs("."); - const base = await Deno.realPath("."); + const fsWatcher = watch(".", { recursive: true }); + const base = await realpath("."); opts = await mergeConfigWithConfigFile(opts); const ignore = await ignoreF(opts); - const changesTimeouts: Record = {}; - async function watchChanges() { - for await (const event of watcher) { - // console.log(">>>> event", event); - const key = event.paths.join(","); - if (changesTimeouts[key]) { - clearTimeout(changesTimeouts[key]); - } - // @ts-ignore - changesTimeouts[key] = setTimeout(async () => { - delete changesTimeouts[key]; - await loadPaths(event.paths); - }, 100); - } + const changesTimeouts: Record> = {}; + function watchChanges() { + return new Promise((_resolve, _reject) => { + fsWatcher.on("change", (_eventType, filename) => { + if (!filename) return; + const filePath = typeof filename === "string" ? filename : filename.toString(); + const key = filePath; + if (changesTimeouts[key]) { + clearTimeout(changesTimeouts[key]); + } + changesTimeouts[key] = setTimeout(async () => { + delete changesTimeouts[key]; + await loadPaths([filePath]); + }, 100); + }); + fsWatcher.on("error", (err) => { + _reject(err); + }); + }); } const flowFolderSuffix = getFolderSuffixWithSep("flow"); @@ -72,8 +76,9 @@ async function dev(opts: GlobalOptions & SyncOptions) { if (paths.length == 0) { return; } - const cpath = (await Deno.realPath(paths[0])).replace(base + SEP, ""); - if (!ignore(cpath, false)) { + const nativePath = (await realpath(paths[0])).replace(base + SEP, ""); + const cpath = nativePath.replaceAll("\\", "/"); + if (!ignore(nativePath, false)) { const typ = getTypeStrFromPath(cpath); log.info("Detected change in " + cpath + " (" + typ + ")"); if (typ == "flow") { @@ -83,13 +88,11 @@ async function dev(opts: GlobalOptions & SyncOptions) { )) as FlowFile; await replaceInlineScripts( localFlow.value.modules, - async (path: string) => await Deno.readTextFile(localPath + path), + async (path: string) => await readFile(localPath + path, "utf-8"), log, localPath, SEP, undefined, - // (path: string, newPath: string) => Deno.renameSync(path, newPath), - // (path: string) => Deno.removeSync(path), ); currentLastEdit = { type: "flow", @@ -99,7 +102,7 @@ async function dev(opts: GlobalOptions & SyncOptions) { log.info("Updated " + localPath); broadcastChanges(currentLastEdit); } else if (typ == "script") { - const content = await Deno.readTextFile(cpath); + const content = await readFile(cpath, "utf-8"); const splitted = cpath.split("."); const wmPath = splitted[0]; const lang = inferContentTypeFromFilePath(cpath, conf.defaultTs); @@ -150,8 +153,10 @@ async function dev(opts: GlobalOptions & SyncOptions) { } async function startApp() { - const app = express.default(); - const server = http.createServer(app); + const server = http.createServer((_req, res) => { + res.writeHead(200); + res.end(); + }); const wss = new WebSocketServer({ server }); // WebSocket server event listeners @@ -224,7 +229,6 @@ const command = new Command() "--includes ", "Filter paths givena glob pattern or path" ) - // deno-lint-ignore no-explicit-any .action(dev as any); export default command; diff --git a/cli/src/commands/flow/flow.ts b/cli/src/commands/flow/flow.ts index d53166dce3..218dc69e8c 100644 --- a/cli/src/commands/flow/flow.ts +++ b/cli/src/commands/flow/flow.ts @@ -1,8 +1,15 @@ -// deno-lint-ignore-file no-explicit-any import { GlobalOptions, isSuperset } from "../../types.ts"; -import { Confirm, SEP, log, yamlStringify } from "../../../deps.ts"; -import { colors, Command, Table, yamlParseFile } from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import { Confirm } from "@cliffy/prompt/confirm"; +import { Table } from "@cliffy/table"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; +import { stringify as yamlStringify } from "@std/yaml"; +import { yamlParseFile } from "../../utils/yaml.ts"; import * as wmill from "../../../gen/services.gen.ts"; +import { readFile } from "node:fs/promises"; +import { mkdirSync, writeFileSync } from "node:fs"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace, validatePath } from "../../core/context.ts"; @@ -51,7 +58,7 @@ export async function pushFlow( await replaceInlineScripts( localFlow.value.modules, - async (path: string) => await Deno.readTextFile(localPath + path), + async (path: string) => await readFile(localPath + path, "utf-8"), log, localPath, SEP @@ -225,7 +232,7 @@ async function preview( // Replace inline scripts with their actual content await replaceInlineScripts( localFlow.value.modules, - async (path: string) => await Deno.readTextFile(flowPath + path), + async (path: string) => await readFile(flowPath + path, "utf-8"), log, flowPath, SEP @@ -286,7 +293,7 @@ async function generateLocks( const ignore = await ignoreF(opts); const elems = Object.keys( await elementsToMap( - await FSFSElement(Deno.cwd(), [], true), + await FSFSElement(process.cwd(), [], true), (p, isD) => { return ( ignore(p, isD) || @@ -348,7 +355,7 @@ export function bootstrap( } const flowDirFullPath = `${flowPath}.flow`; - Deno.mkdirSync(flowDirFullPath, { recursive: false }); + mkdirSync(flowDirFullPath, { recursive: false }); const newFlowDefinition = defaultFlowDefinition(); if (opts.summary !== undefined) { @@ -363,7 +370,7 @@ export function bootstrap( ); const flowYamlPath = `${flowDirFullPath}/flow.yaml`; - Deno.writeTextFile(flowYamlPath, newFlowDefinitionYaml, { createNew: true }); + writeFileSync(flowYamlPath, newFlowDefinitionYaml, { flag: "wx", encoding: "utf-8" }); } const command = new Command() diff --git a/cli/src/commands/flow/flow_metadata.ts b/cli/src/commands/flow/flow_metadata.ts index 26883f7512..34771be346 100644 --- a/cli/src/commands/flow/flow_metadata.ts +++ b/cli/src/commands/flow/flow_metadata.ts @@ -1,11 +1,10 @@ -import { - SEP, - colors, - log, - path, - yamlParseFile, - yamlStringify, -} from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import * as path from "@std/path"; +import { SEPARATOR as SEP } from "@std/path"; +import { stringify as yamlStringify } from "@std/yaml"; +import { yamlParseFile } from "../../utils/yaml.ts"; +import { readFile } from "node:fs/promises"; import { GlobalOptions } from "../../types.ts"; import { readLockfile, @@ -37,7 +36,7 @@ async function generateFlowHash( folder: string, defaultTs: "bun" | "deno" | undefined ) { - const elems = await FSFSElement(path.join(Deno.cwd(), folder), [], true); + const elems = await FSFSElement(path.join(process.cwd(), folder), [], true); const hashes: Record = {}; for await (const f of elems.getChildren()) { if (exts.some((e) => f.path.endsWith(e))) { @@ -124,13 +123,11 @@ export async function generateFlowLockInternal( log.info(`Recomputing locks of ${changedScripts.join(", ")} in ${folder}`); await replaceInlineScripts( flowValue.value.modules, - async (path: string) => await Deno.readTextFile(folder + SEP + path), + async (path: string) => await readFile(folder + SEP + path, "utf-8"), log, folder + SEP!, SEP, changedScripts - // (path: string, newPath: string) => Deno.renameSync(path, newPath), - // (path: string) => Deno.removeSync(path) ); //removeChangedLocks @@ -148,12 +145,12 @@ export async function generateFlowLockInternal( opts.defaultTs ); inlineScripts.forEach((s) => { - writeIfChanged(Deno.cwd() + SEP + folder + SEP + s.path, s.content); + writeIfChanged(process.cwd() + SEP + folder + SEP + s.path, s.content); }); // Overwrite `flow.yaml` with the new lockfile references writeIfChanged( - Deno.cwd() + SEP + folder + SEP + "flow.yaml", + process.cwd() + SEP + folder + SEP + "flow.yaml", yamlStringify(flowValue as Record) ); } diff --git a/cli/src/commands/folder/folder.ts b/cli/src/commands/folder/folder.ts index 421d142326..1073b2626a 100644 --- a/cli/src/commands/folder/folder.ts +++ b/cli/src/commands/folder/folder.ts @@ -1,5 +1,10 @@ -// deno-lint-ignore-file no-explicit-any -import { colors, Command, log, SEP, Table } from "../../../deps.ts"; +import { stat } from "node:fs/promises"; + +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import { Table } from "@cliffy/table"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; import * as wmill from "../../../gen/services.gen.ts"; import { requireLogin } from "../../core/auth.ts"; @@ -103,8 +108,8 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) { return; } - const fstat = await Deno.stat(filePath); - if (!fstat.isFile) { + const fstat = await stat(filePath); + if (!fstat.isFile()) { throw new Error("file path must refer to a file."); } diff --git a/cli/src/commands/gitsync-settings/gitsync-settings.ts b/cli/src/commands/gitsync-settings/gitsync-settings.ts index f943bb40b7..5c27e032b5 100644 --- a/cli/src/commands/gitsync-settings/gitsync-settings.ts +++ b/cli/src/commands/gitsync-settings/gitsync-settings.ts @@ -1,4 +1,4 @@ -import { Command } from "../../../deps.ts"; +import { Command } from "@cliffy/command"; import { pullGitSyncSettings } from "./pull.ts"; import { pushGitSyncSettings } from "./push.ts"; diff --git a/cli/src/commands/gitsync-settings/legacySettings.ts b/cli/src/commands/gitsync-settings/legacySettings.ts index fe6f29faa5..929d0e473e 100644 --- a/cli/src/commands/gitsync-settings/legacySettings.ts +++ b/cli/src/commands/gitsync-settings/legacySettings.ts @@ -1,4 +1,7 @@ -import { colors, Confirm } from "../../../deps.ts"; +import process from "node:process"; + +import { colors } from "@cliffy/ansi/colors"; +import { Confirm } from "@cliffy/prompt/confirm"; import * as wmill from "../../../gen/services.gen.ts"; import { GitSyncRepository } from "./types.ts"; @@ -24,7 +27,7 @@ export async function handleLegacyRepositoryMigration( const workspaceIncludePath = gitSyncSettings.include_path; const workspaceIncludeType = gitSyncSettings.include_type; - if (Deno.stdout.isTerminal() && !opts.yes) { + if (!!process.stdout.isTTY && !opts.yes) { // Interactive mode - show migration prompt console.log(colors.yellow('\n⚠️ Legacy git-sync settings detected!')); console.log(`\nRepository "${selectedRepo.git_repo_resource_path}" has legacy settings format.`); @@ -139,6 +142,6 @@ export async function handleLegacyRepositoryMigration( console.error('3. Push local settings to override backend settings:'); console.error(' wmill gitsync-settings push\n'); } - Deno.exit(1); + process.exit(1); } } \ No newline at end of file diff --git a/cli/src/commands/gitsync-settings/pull.ts b/cli/src/commands/gitsync-settings/pull.ts index 4bcd8900d0..7c58a45bbe 100644 --- a/cli/src/commands/gitsync-settings/pull.ts +++ b/cli/src/commands/gitsync-settings/pull.ts @@ -1,4 +1,7 @@ -import { colors, log, yamlStringify } from "../../../deps.ts"; +import { writeFile } from "node:fs/promises"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { stringify as yamlStringify } from "@std/yaml"; import { GlobalOptions } from "../../types.ts"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace } from "../../core/context.ts"; @@ -173,7 +176,7 @@ export async function pullGitSyncSettings( } // Write the new configuration - await Deno.writeTextFile("wmill.yaml", yamlStringify(updatedConfig)); + await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8"); if (opts.jsonOutput) { console.log( @@ -286,7 +289,7 @@ export async function pullGitSyncSettings( ); const hasConflict = !deepEqual(gitSyncBackend, gitSyncCurrent); - if (hasConflict && !opts.yes && Deno.stdin.isTerminal()) { + if (hasConflict && !opts.yes && !!process.stdin.isTTY) { // Show the diff first log.info("Changes that would be applied locally:"); const changes = generateChanges(effectiveCurrentSettings, backendSyncOptions); @@ -295,7 +298,7 @@ export async function pullGitSyncSettings( } // Interactive mode - ask user - const { Select } = await import("../../../deps.ts"); + const { Select } = await import("@cliffy/prompt/select"); const choice = await Select.prompt({ message: "Settings conflict detected. How would you like to proceed?", options: [ @@ -369,7 +372,7 @@ export async function pullGitSyncSettings( } // Write updated configuration - await Deno.writeTextFile("wmill.yaml", yamlStringify(updatedConfig)); + await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8"); if (opts.jsonOutput) { console.log( @@ -446,7 +449,7 @@ export async function pullGitSyncSettings( } // Write updated configuration - await Deno.writeTextFile("wmill.yaml", yamlStringify(updatedConfig)); + await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8"); if (opts.jsonOutput) { console.log( diff --git a/cli/src/commands/gitsync-settings/push.ts b/cli/src/commands/gitsync-settings/push.ts index 2e7f050207..62a3b2781a 100644 --- a/cli/src/commands/gitsync-settings/push.ts +++ b/cli/src/commands/gitsync-settings/push.ts @@ -1,4 +1,8 @@ -import { colors, log, Confirm } from "../../../deps.ts"; +import process from "node:process"; + +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { Confirm } from "@cliffy/prompt/confirm"; import { GlobalOptions } from "../../types.ts"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace } from "../../core/context.ts"; @@ -34,7 +38,7 @@ export async function pushGitSyncSettings( } catch (error) { if (error instanceof Error && error.message.includes("overrides")) { log.error(error.message); - Deno.exit(1); + process.exit(1); } throw error; } @@ -51,7 +55,7 @@ export async function pushGitSyncSettings( "No wmill.yaml file found. Please run 'wmill init' first to create the configuration file.", ), ); - Deno.exit(1); + process.exit(1); } // Read local configuration @@ -247,7 +251,7 @@ export async function pushGitSyncSettings( } // Ask for confirmation unless --yes is passed or not in TTY - if (!opts.yes && Deno.stdin.isTerminal()) { + if (!opts.yes && !!process.stdin.isTTY) { const confirmed = await Confirm.prompt({ message: `Do you want to apply these changes to the remote?`, default: true, diff --git a/cli/src/commands/gitsync-settings/utils.ts b/cli/src/commands/gitsync-settings/utils.ts index 10b9cf351b..e2acd5e506 100644 --- a/cli/src/commands/gitsync-settings/utils.ts +++ b/cli/src/commands/gitsync-settings/utils.ts @@ -1,4 +1,5 @@ -import { colors, log } from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; import { deepEqual, selectRepository } from "../../utils/utils.ts"; import { SyncOptions, getEffectiveSettings, DEFAULT_SYNC_OPTIONS } from "../../core/conf.ts"; import { GitSyncRepository, GIT_SYNC_FIELDS } from "./types.ts"; diff --git a/cli/src/commands/hub/hub.ts b/cli/src/commands/hub/hub.ts index 314d781bba..51aa59d133 100644 --- a/cli/src/commands/hub/hub.ts +++ b/cli/src/commands/hub/hub.ts @@ -1,5 +1,5 @@ -// deno-lint-ignore-file no-explicit-any -import { Command, log } from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import * as log from "@std/log"; import * as wmill from "../../../gen/services.gen.ts"; import { requireLogin } from "../../core/auth.ts"; diff --git a/cli/src/commands/init/init.ts b/cli/src/commands/init/init.ts index 0bd2443716..81eadbe732 100644 --- a/cli/src/commands/init/init.ts +++ b/cli/src/commands/init/init.ts @@ -1,4 +1,9 @@ -import { colors, Command, log, yamlStringify, Confirm } from "../../../deps.ts"; +import { stat, writeFile, rm, mkdir } from "node:fs/promises"; +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import { Confirm } from "@cliffy/prompt/confirm"; +import * as log from "@std/log"; +import { stringify as yamlStringify } from "@std/yaml"; import { GlobalOptions } from "../../types.ts"; import { readLockfile } from "../../utils/metadata.ts"; import { getActiveWorkspaceOrFallback } from "../workspace/workspace.ts"; @@ -36,7 +41,7 @@ export interface InitOptions { * Bootstrap a windmill project with a wmill.yaml file */ async function initAction(opts: InitOptions) { - if (await Deno.stat("wmill.yaml").catch(() => null)) { + if (await stat("wmill.yaml").catch(() => null)) { log.error(colors.red("wmill.yaml already exists")); } else { // Import DEFAULT_SYNC_OPTIONS from conf.ts @@ -63,7 +68,7 @@ async function initAction(opts: InitOptions) { } initialConfig.nonDottedPaths = true; - await Deno.writeTextFile("wmill.yaml", yamlStringify(initialConfig)); + await writeFile("wmill.yaml", yamlStringify(initialConfig), "utf-8"); log.info(colors.green("wmill.yaml created with default settings")); // Create lock file @@ -80,12 +85,12 @@ async function initAction(opts: InitOptions) { const shouldBind = opts.bindProfile === true; const shouldPrompt = opts.bindProfile === undefined && - Deno.stdin.isTerminal() && + !!process.stdin.isTTY && !opts.useDefault; const shouldSkip = opts.bindProfile != true && - (opts.useDefault || !Deno.stdin.isTerminal()); + (opts.useDefault || !!!process.stdin.isTTY); if (!shouldSkip) { // Show workspace info if we're binding or prompting @@ -132,7 +137,7 @@ async function initAction(opts: InitOptions) { currentConfig.gitBranches[currentBranch].workspaceId = activeWorkspace.workspaceId; - await Deno.writeTextFile("wmill.yaml", yamlStringify(currentConfig)); + await writeFile("wmill.yaml", yamlStringify(currentConfig), "utf-8"); log.info( colors.green( @@ -183,7 +188,7 @@ async function initAction(opts: InitOptions) { if (useBackendSettings === undefined) { // Interactive prompt - const { Select } = await import("../../../deps.ts"); + const { Select } = await import("@cliffy/prompt/select"); const choice = await Select.prompt({ message: "Git-sync settings found on backend. What would you like to do?", @@ -206,13 +211,13 @@ async function initAction(opts: InitOptions) { if (choice === "cancel") { // Clean up the created files try { - await Deno.remove("wmill.yaml"); - await Deno.remove("wmill-lock.yaml"); + await rm("wmill.yaml"); + await rm("wmill-lock.yaml"); } catch (e) { // Ignore cleanup errors } log.info("Init cancelled"); - Deno.exit(0); + process.exit(0); } useBackendSettings = choice === "backend"; @@ -256,32 +261,32 @@ async function initAction(opts: InitOptions) { ).join("\n"); // Create AGENTS.md file with minimal instructions - if (!(await Deno.stat("AGENTS.md").catch(() => null))) { - await Deno.writeTextFile( + if (!(await stat("AGENTS.md").catch(() => null))) { + await writeFile( "AGENTS.md", - generateAgentsMdContent(skillsReference) + generateAgentsMdContent(skillsReference), "utf-8" ); log.info(colors.green("Created AGENTS.md")); } // Create CLAUDE.md file, referencing AGENTS.md - if (!(await Deno.stat("CLAUDE.md").catch(() => null))) { - await Deno.writeTextFile( + if (!(await stat("CLAUDE.md").catch(() => null))) { + await writeFile( "CLAUDE.md", `Instructions are in @AGENTS.md -` +`, "utf-8" ); log.info(colors.green("Created CLAUDE.md")); } // Create .claude/skills/ directory and skill files try { - await Deno.mkdir(".claude/skills", { recursive: true }); + await mkdir(".claude/skills", { recursive: true }); await Promise.all( SKILLS.map(async (skill) => { const skillDir = `.claude/skills/${skill.name}`; - await Deno.mkdir(skillDir, { recursive: true }); + await mkdir(skillDir, { recursive: true }); let skillContent = SKILL_CONTENT[skill.name]; if (skillContent) { @@ -304,7 +309,7 @@ async function initAction(opts: InitOptions) { } } - await Deno.writeTextFile(`${skillDir}/SKILL.md`, skillContent); + await writeFile(`${skillDir}/SKILL.md`, skillContent, "utf-8"); } }) ); diff --git a/cli/src/commands/instance/instance.ts b/cli/src/commands/instance/instance.ts index 696f1b1c85..c6b848d379 100644 --- a/cli/src/commands/instance/instance.ts +++ b/cli/src/commands/instance/instance.ts @@ -1,16 +1,17 @@ -import { - Command, - Confirm, - path, - Select, - setClient, - Table, - yamlParseFile, - yamlStringify, -} from "../../../deps.ts"; +import { readFile, writeFile, readdir, mkdir, rm, stat } from "node:fs/promises"; +import { appendFile } from "node:fs/promises"; +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import { Confirm } from "@cliffy/prompt/confirm"; +import { Input } from "@cliffy/prompt/input"; +import { Select } from "@cliffy/prompt/select"; +import { Table } from "@cliffy/table"; +import * as log from "@std/log"; +import * as path from "@std/path"; +import { stringify as yamlStringify } from "@std/yaml"; +import { setClient } from "../../core/client.ts"; +import { yamlParseFile } from "../../utils/yaml.ts"; import * as wmill from "../../../gen/services.gen.ts"; - -import { colors, Input, log } from "../../../deps.ts"; import { loginInteractive } from "../../core/login.ts"; import { getActiveInstanceFilePath, @@ -51,7 +52,7 @@ export interface Instance { export async function allInstances(): Promise { try { const file = await getInstancesConfigFilePath(); - const txt = await Deno.readTextFile(file); + const txt = await readFile(file, "utf-8"); return txt .split("\n") .map((line) => { @@ -118,26 +119,19 @@ export async function addInstance( async function appendInstance(instance: Instance) { instance.remote = new URL(instance.remote).toString(); // add trailing slash in all cases! await removeInstance(instance.name); - const file = await Deno.open(await getInstancesConfigFilePath(), { - append: true, - write: true, - read: true, - create: true, - }); - await file.write(new TextEncoder().encode(JSON.stringify(instance) + "\n")); - - file.close(); + const filePath = await getInstancesConfigFilePath(); + await appendFile(filePath, JSON.stringify(instance) + "\n", "utf-8"); } async function removeInstance(name: string) { const orgWorkspaces = await allInstances(); - await Deno.writeTextFile( + await writeFile( await getInstancesConfigFilePath(), orgWorkspaces .filter((x) => x.name !== name) .map((x) => JSON.stringify(x)) - .join("\n") + "\n", + .join("\n") + "\n", "utf-8", ); } @@ -289,7 +283,7 @@ async function instancePull(opts: InstanceSyncOptions) { const totalChanges = uChanges + sChanges + cChanges + gChanges; - const rootDir = Deno.cwd(); + const rootDir = process.cwd(); if (totalChanges > 0) { let confirm = true; @@ -308,7 +302,7 @@ async function instancePull(opts: InstanceSyncOptions) { if (confirm) { if (uChanges > 0) { if (opts.folderPerInstance && opts.prefixSettings) { - await Deno.mkdir(path.join(rootDir, opts.prefix), { + await mkdir(path.join(rootDir, opts.prefix), { recursive: true, }); } @@ -348,10 +342,10 @@ async function instancePull(opts: InstanceSyncOptions) { const workspaceName = opts?.folderPerInstance ? instance.prefix + "/" + remoteWorkspace.id : instance.prefix + "_" + remoteWorkspace.id; - await Deno.mkdir(path.join(rootDir, workspaceName), { + await mkdir(path.join(rootDir, workspaceName), { recursive: true, }); - await Deno.chdir(path.join(rootDir, workspaceName)); + process.chdir(path.join(rootDir, workspaceName)); await addWorkspace( { remote: instance.remote, @@ -397,7 +391,7 @@ async function instancePull(opts: InstanceSyncOptions) { if (confirmDelete) { for (const workspace of localWorkspacesToDelete) { await removeWorkspace(workspace.id, false, {}); - await Deno.remove(path.join(rootDir, workspace.dir), { + await rm(path.join(rootDir, workspace.dir), { recursive: true, }); } @@ -467,7 +461,7 @@ async function instancePush(opts: InstanceSyncOptions) { if (opts.includeWorkspaces) { instances = await allInstances(); - const rootDir = Deno.cwd(); + const rootDir = process.cwd(); let localPrefix; if (opts.prefix) { @@ -506,7 +500,7 @@ async function instancePush(opts: InstanceSyncOptions) { for (const localWorkspace of localWorkspaces) { log.info("\nPushing workspace " + localWorkspace.id); try { - await Deno.chdir(path.join(rootDir, localWorkspace.dir)); + process.chdir(path.join(rootDir, localWorkspace.dir)); } catch (_) { throw new Error( "Workspace folder not found, are you in the right directory?", @@ -515,7 +509,7 @@ async function instancePush(opts: InstanceSyncOptions) { try { const workspaceSettings = (await yamlParseFile( - path.join(Deno.cwd(), "settings.yaml"), + path.join(process.cwd(), "settings.yaml"), )) as SimplifiedSettings; await workspaceSetup( { @@ -586,12 +580,13 @@ async function getLocalWorkspaces( ) { const localWorkspaces: { dir: string; id: string }[] = []; - if (!(await Deno.stat(localPrefix).catch(() => null))) { - await Deno.mkdir(localPrefix); + if (!(await stat(localPrefix).catch(() => null))) { + await mkdir(localPrefix); } if (folderPerInstance) { - for await (const dir of Deno.readDir(rootDir + "/" + localPrefix)) { - if (dir.isDirectory) { + const prefixEntries = await readdir(rootDir + "/" + localPrefix, { withFileTypes: true }); + for (const dir of prefixEntries) { + if (dir.isDirectory()) { const dirName = dir.name; localWorkspaces.push({ dir: localPrefix + "/" + dirName, @@ -600,7 +595,8 @@ async function getLocalWorkspaces( } } } else { - for await (const dir of Deno.readDir(rootDir)) { + const rootEntries = await readdir(rootDir, { withFileTypes: true }); + for (const dir of rootEntries) { const dirName = dir.name; if (dirName.startsWith(localPrefix + "_")) { localWorkspaces.push({ @@ -631,9 +627,9 @@ async function switchI(opts: {}, instanceName: string) { return; } - await Deno.writeTextFile( + await writeFile( await getActiveInstanceFilePath(), - instanceName, + instanceName, "utf-8", ); log.info(colors.green.underline(`Switched to instance ${instanceName}`)); @@ -646,7 +642,7 @@ export async function getActiveInstance(opts: { return opts.instance; } try { - return await Deno.readTextFile(await getActiveInstanceFilePath()); + return await readFile(await getActiveInstanceFilePath(), "utf-8"); } catch { return undefined; } @@ -657,7 +653,7 @@ async function getConfig(opts: InstanceSyncOptions & { outputFile?: string }) { const config = await wmill.getInstanceConfig(); const yaml = yamlStringify(config as Record); if (opts.outputFile) { - await Deno.writeTextFile(opts.outputFile, yaml); + await writeFile(opts.outputFile, yaml, "utf-8"); log.info(colors.green(`Instance config written to ${opts.outputFile}`)); } else { console.log(yaml); diff --git a/cli/src/commands/jobs/jobs.ts b/cli/src/commands/jobs/jobs.ts index 495e513c87..083426479d 100644 --- a/cli/src/commands/jobs/jobs.ts +++ b/cli/src/commands/jobs/jobs.ts @@ -1,8 +1,10 @@ -// deno-lint-ignore-file no-explicit-any import { GlobalOptions } from "../../types.ts"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace } from "../../core/context.ts"; -import { colors, Command, Confirm, log } from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import { colors } from "@cliffy/ansi/colors"; +import { Confirm } from "@cliffy/prompt/confirm"; +import * as log from "@std/log"; import { mergeConfigWithConfigFile } from "../../core/conf.ts"; import * as fs from "node:fs/promises"; import * as wmill from "../../../gen/services.gen.ts"; diff --git a/cli/src/commands/lint/lint.ts b/cli/src/commands/lint/lint.ts index b281587675..6ce244c7c0 100644 --- a/cli/src/commands/lint/lint.ts +++ b/cli/src/commands/lint/lint.ts @@ -1,11 +1,12 @@ -import { - colors, - Command, - log, - path, - SEP, - yamlParseFile, -} from "../../../deps.ts"; +import { stat, readdir } from "node:fs/promises"; +import process from "node:process"; + +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import * as log from "@std/log"; +import * as path from "@std/path"; +import { SEPARATOR as SEP } from "@std/path"; +import { yamlParseFile } from "../../utils/yaml.ts"; import { GlobalOptions } from "../../types.ts"; import { mergeConfigWithConfigFile } from "../../core/conf.ts"; import { @@ -17,7 +18,7 @@ import { getValidationTargetFromFilename, type ValidationTarget, WindmillYamlValidator, -} from "npm:windmill-yaml-validator@1.1.1"; +} from "windmill-yaml-validator"; import { inferContentTypeFromFilePath, languageNeedsLock, @@ -159,8 +160,8 @@ async function checkInlineFile( ): Promise { const fullPath = path.join(baseDir, relativePath.trim()); try { - const stat = await Deno.stat(fullPath); - return stat.size > 0; + const s = await stat(fullPath); + return s.size > 0; } catch { return false; } @@ -272,8 +273,9 @@ async function checkRawAppRunnables( const issues: FileIssue[] = []; const allFiles: string[] = []; - for await (const entry of Deno.readDir(backendDir)) { - if (entry.isFile) { + const entries = await readdir(backendDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isFile()) { allFiles.push(entry.name); } } @@ -316,8 +318,8 @@ async function checkRawAppRunnables( const lockFile = path.join(backendDir, `${runnableId}.lock`); let hasLock = false; try { - const stat = await Deno.stat(lockFile); - hasLock = stat.size > 0; + const s = await stat(lockFile); + hasLock = s.size > 0; } catch { // No lock file } @@ -375,8 +377,8 @@ async function checkRawAppRunnables( const lockFile = path.join(backendDir, `${runnableId}.lock`); let hasLock = false; try { - const stat = await Deno.stat(lockFile); - hasLock = stat.size > 0; + const s = await stat(lockFile); + hasLock = s.size > 0; } catch { // No lock file } @@ -404,10 +406,10 @@ export async function checkMissingLocks( opts: GlobalOptions & { defaultTs?: "bun" | "deno" }, directory?: string, ): Promise { - const initialCwd = Deno.cwd(); + const initialCwd = process.cwd(); const targetDirectory = directory ? path.resolve(initialCwd, directory) - : Deno.cwd(); + : process.cwd(); const { ...syncOpts } = opts; const mergedOpts = await mergeConfigWithConfigFile(syncOpts); @@ -483,7 +485,7 @@ export async function checkMissingLocks( let language: ScriptLanguage | null = null; for (const ext of exts) { try { - await Deno.stat(path.join(targetDirectory, basePath + ext)); + await stat(path.join(targetDirectory, basePath + ext)); language = inferContentTypeFromFilePath(basePath + ext, defaultTs); break; } catch { @@ -582,7 +584,7 @@ export async function checkMissingLocks( const backendDir = path.join(rawAppDir, "backend"); try { - await Deno.stat(backendDir); + await stat(backendDir); } catch { continue; // No backend folder } @@ -606,20 +608,20 @@ export async function runLint( opts: LintOptions, directory?: string, ): Promise { - const initialCwd = Deno.cwd(); + const initialCwd = process.cwd(); const explicitTargetDirectory = directory ? path.resolve(initialCwd, directory) : undefined; const { json: _json, ...syncOpts } = opts; const mergedOpts = await mergeConfigWithConfigFile(syncOpts); - const targetDirectory = explicitTargetDirectory ?? Deno.cwd(); + const targetDirectory = explicitTargetDirectory ?? process.cwd(); - const stats = await Deno.stat(targetDirectory).catch(() => null); + const stats = await stat(targetDirectory).catch(() => null); if (!stats) { throw new Error(`Directory not found: ${targetDirectory}`); } - if (!stats.isDirectory) { + if (!stats.isDirectory()) { throw new Error(`Path is not a directory: ${targetDirectory}`); } @@ -745,7 +747,7 @@ async function lint(opts: LintOptions, directory?: string) { const report = await runLint(opts, directory); printReport(report, !!opts.json); if (report.exitCode !== 0) { - Deno.exit(report.exitCode); + process.exit(report.exitCode); } } catch (error) { const message = error instanceof Error ? error.message : String(error); @@ -764,7 +766,7 @@ async function lint(opts: LintOptions, directory?: string) { } else { log.error(colors.red(`❌ ${message}`)); } - Deno.exit(1); + process.exit(1); } } diff --git a/cli/src/commands/queues/queues.ts b/cli/src/commands/queues/queues.ts index 1afcaea269..4f9201a8ab 100644 --- a/cli/src/commands/queues/queues.ts +++ b/cli/src/commands/queues/queues.ts @@ -1,5 +1,6 @@ -import { Command, Table } from "../../../deps.ts"; -import { log } from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import { Table } from "@cliffy/table"; +import * as log from "@std/log"; import * as wmill from "../../../gen/services.gen.ts"; import { pickInstance } from "../instance/instance.ts"; diff --git a/cli/src/commands/resource-type/resource-type.ts b/cli/src/commands/resource-type/resource-type.ts index daeaa3386d..2a4a785ab2 100644 --- a/cli/src/commands/resource-type/resource-type.ts +++ b/cli/src/commands/resource-type/resource-type.ts @@ -1,6 +1,5 @@ -// deno-lint-ignore-file no-explicit-any - import { writeFileSync } from "node:fs"; +import { stat } from "node:fs/promises"; import path from "node:path"; import process from "node:process"; @@ -12,7 +11,10 @@ import { } from "../../types.ts"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace } from "../../core/context.ts"; -import { colors, Command, log, Table } from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import { Table } from "@cliffy/table"; +import * as log from "@std/log"; import * as wmill from "../../../gen/services.gen.ts"; import { ResourceType } from "../../../gen/types.gen.ts"; import { compileResourceTypeToTsType } from "../../utils/resource_types.ts"; @@ -65,8 +67,8 @@ export async function pushResourceType( type PushOptions = GlobalOptions; async function push(opts: PushOptions, filePath: string, name: string) { - const fstat = await Deno.stat(filePath); - if (!fstat.isFile) { + const fstat = await stat(filePath); + if (!fstat.isFile()) { throw new Error("file path must refer to a file."); } const workspace = await resolveWorkspace(opts); diff --git a/cli/src/commands/resource/resource.ts b/cli/src/commands/resource/resource.ts index 4a62a28b70..1d16aac782 100644 --- a/cli/src/commands/resource/resource.ts +++ b/cli/src/commands/resource/resource.ts @@ -1,4 +1,5 @@ -// deno-lint-ignore-file no-explicit-any +import { stat } from "node:fs/promises"; + import { GlobalOptions, isSuperset, @@ -7,7 +8,11 @@ import { } from "../../types.ts"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace, validatePath } from "../../core/context.ts"; -import { colors, Command, log, SEP, Table } from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import { Table } from "@cliffy/table"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; import * as wmill from "../../../gen/services.gen.ts"; import { Resource } from "../../../gen/types.gen.ts"; import { readInlinePathSync } from "../../utils/utils.ts"; @@ -109,8 +114,8 @@ async function push(opts: PushOptions, filePath: string, remotePath: string) { return; } - const fstat = await Deno.stat(filePath); - if (!fstat.isFile) { + const fstat = await stat(filePath); + if (!fstat.isFile()) { throw new Error("file path must refer to a file."); } diff --git a/cli/src/commands/schedule/schedule.ts b/cli/src/commands/schedule/schedule.ts index bebf37aa9e..bd5192de66 100644 --- a/cli/src/commands/schedule/schedule.ts +++ b/cli/src/commands/schedule/schedule.ts @@ -1,5 +1,10 @@ -// deno-lint-ignore-file no-explicit-any -import { colors, Command, log, SEP, Table } from "../../../deps.ts"; +import { stat } from "node:fs/promises"; + +import { Command } from "@cliffy/command"; +import { Table } from "@cliffy/table"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace, validatePath } from "../../core/context.ts"; import * as wmill from "../../../gen/services.gen.ts"; @@ -114,8 +119,8 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) { return; } - const fstat = await Deno.stat(filePath); - if (!fstat.isFile) { + const fstat = await stat(filePath); + if (!fstat.isFile()) { throw new Error("file path must refer to a file."); } diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index ff962a3eeb..ff91b11fb5 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -1,18 +1,15 @@ -// deno-lint-ignore-file no-explicit-any import { GlobalOptions } from "../../types.ts"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace, validatePath } from "../../core/context.ts"; -import { - colors, - Command, - Confirm, - log, - readAll, - SEP, - Table, - writeAllSync, - yamlStringify, -} from "../../../deps.ts"; +import { readFile, writeFile, stat } from "node:fs/promises"; +import { Buffer } from "node:buffer"; +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import { Confirm } from "@cliffy/prompt/confirm"; +import { Table } from "@cliffy/table"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; +import { stringify as yamlStringify } from "@std/yaml"; import { deepEqual } from "../../utils/utils.ts"; import * as wmill from "../../../gen/services.gen.ts"; import * as specificItems from "../../core/specific_items.ts"; @@ -51,7 +48,7 @@ import { } from "../../core/conf.ts"; import { SyncCodebase, listSyncCodebases } from "../../utils/codebase.ts"; import fs from "node:fs"; -import { type Tarball } from "npm:@ayonli/jsext/archive"; +import { type Tarball } from "@ayonli/jsext/archive"; import { execSync } from "node:child_process"; import { NewScript, Script } from "../../../gen/types.gen.ts"; @@ -106,8 +103,8 @@ async function push(opts: PushOptions, filePath: string) { return; } - const fstat = await Deno.stat(filePath); - if (!fstat.isFile) { + const fstat = await stat(filePath); + if (!fstat.isFile()) { throw new Error("file path must refer to a file."); } @@ -159,9 +156,9 @@ export async function findResourceFile(path: string) { const validCandidates = ( await Promise.all( candidates.map((x) => { - return Deno.stat(x) + return stat(x) .catch(() => undefined) - .then((x) => x?.isFile) + .then((x) => x?.isFile()) .then((e) => { return { path: x, file: e }; }); @@ -261,7 +258,7 @@ export async function handleFile( }).toString(); log.info("Custom bundler executed for " + path); } else { - const esbuild = await import("npm:esbuild@0.24.2"); + const esbuild = await import("esbuild"); log.info(`Started bundling ${path} ...`); const startTime = performance.now(); @@ -295,7 +292,7 @@ export async function handleFile( ); } if (outputFiles.length > 1) { - const archiveNpm = await import("npm:@ayonli/jsext/archive"); + const archiveNpm = await import("@ayonli/jsext/archive"); log.info( `Found multiple output files for ${path}, creating a tarball... ${outputFiles .map((file) => file.path) @@ -314,7 +311,7 @@ export async function handleFile( continue; } log.info(`Adding file: ${file.path.substring(1)}`); - // deno-lint-ignore no-explicit-any + const fil = new File([file.contents as any], file.path.substring(1)); tarball.append(fil); } @@ -327,7 +324,7 @@ export async function handleFile( bundleContent = tarball; } else { if (Array.isArray(codebase.assets) && codebase.assets.length > 0) { - const archiveNpm = await import("npm:@ayonli/jsext/archive"); + const archiveNpm = await import("@ayonli/jsext/archive"); log.info( `Using the following asset configuration for ${path}: ${JSON.stringify( codebase.assets @@ -384,7 +381,7 @@ export async function handleFile( } catch { log.debug(`Script ${remotePath} does not exist on remote`); } - const content = await Deno.readTextFile(path); + const content = await readFile(path, "utf-8"); if (opts?.skipScriptsMetadata) { // if (codebase) { @@ -392,17 +389,6 @@ export async function handleFile( // await updateScriptSchema(content, language, typed, path); // if (typedBefore != typed.schema) { // log.info(`Updated metadata for bundle ${path}`); - // showDiff( - // yamlStringify(typedBefore, yamlOptions), - // yamlStringify(typed.schema, yamlOptions) - // ); - // await Deno.writeTextFile( - // remotePath + ".script.yaml", - // yamlStringify(typed as Record, yamlOptions) - // ); - // } - // } - // else { typed = structuredClone(remote); // } } @@ -544,7 +530,7 @@ async function streamToBlob(stream: ReadableStream): Promise { chunks.push(value); } - // deno-lint-ignore no-explicit-any + const blob = new Blob(chunks as any); return blob; } @@ -611,9 +597,9 @@ export async function findContentFile(filePath: string) { const validCandidates = ( await Promise.all( candidates.map((x) => { - return Deno.stat(x) + return stat(x) .catch(() => undefined) - .then((x) => x?.isFile) + .then((x) => x?.isFile()) .then((e) => { return { path: x, file: e }; }); @@ -778,10 +764,12 @@ export async function resolve(input: string): Promise> { } if (input == "@-") { - input = new TextDecoder().decode(await readAll(Deno.stdin)); + const chunks: Buffer[] = []; + for await (const chunk of process.stdin) chunks.push(chunk); + input = new TextDecoder().decode(Buffer.concat(chunks)); } if (input[0] == "@") { - input = await Deno.readTextFile(input.substring(1)); + input = await readFile(input.substring(1), "utf-8"); } try { return JSON.parse(input); @@ -830,7 +818,7 @@ async function run( break; } catch { - new Promise((resolve, _) => setTimeout(() => resolve(undefined), 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); } } } @@ -872,6 +860,7 @@ export async function track_job(workspace: string, id: string) { log.info("failed to get job updated. skipping log streaming."); break; } + await new Promise((resolve) => setTimeout(resolve, 500)); continue; } @@ -881,7 +870,7 @@ export async function track_job(workspace: string, id: string) { } if (updates.new_logs) { - writeAllSync(Deno.stdout, new TextEncoder().encode(updates.new_logs)); + process.stdout.write(updates.new_logs); logOffset += updates.new_logs.length; } @@ -951,8 +940,8 @@ async function bootstrap( const scriptMetadataFileFullPath = scriptPath + ".script.yaml"; try { - await Deno.stat(scriptCodeFileFullPath); - await Deno.stat(scriptMetadataFileFullPath); + await stat(scriptCodeFileFullPath); + await stat(scriptMetadataFileFullPath); throw new Error("File already exists in repository"); } catch { // file does not exist, we can continue @@ -971,14 +960,14 @@ async function bootstrap( yamlOptions ); - await Deno.writeTextFile(scriptCodeFileFullPath, scriptInitialCode, { - createNew: true, + await writeFile(scriptCodeFileFullPath, scriptInitialCode, { + flag: 'wx', encoding: 'utf-8', }); - await Deno.writeTextFile( + await writeFile( scriptMetadataFileFullPath, scriptInitialMetadataYaml, { - createNew: true, + flag: 'wx', encoding: 'utf-8', } ); } @@ -1028,7 +1017,7 @@ async function generateMetadata( // TODO: test this as well. const ignore = await ignoreF(opts); const elems = await elementsToMap( - await FSFSElement(Deno.cwd(), codebases, false), + await FSFSElement(process.cwd(), codebases, false), (p, isD) => { return ( (!isD && !exts.some((ext) => p.endsWith(ext))) || @@ -1107,8 +1096,8 @@ async function preview( return; } - const fstat = await Deno.stat(filePath); - if (!fstat.isFile) { + const fstat = await stat(filePath); + if (!fstat.isFile()) { throw new Error("file path must refer to a file."); } @@ -1120,7 +1109,7 @@ async function preview( const codebases = await listSyncCodebases(opts); const language = inferContentTypeFromFilePath(filePath, opts?.defaultTs); - const content = await Deno.readTextFile(filePath); + const content = await readFile(filePath, "utf-8"); const input = opts.data ? await resolve(opts.data) : {}; // Check if this is a codebase script @@ -1139,7 +1128,7 @@ async function preview( maxBuffer: 1024 * 1024 * 50, }).toString(); } else { - const esbuild = await import("npm:esbuild@0.24.2"); + const esbuild = await import("esbuild"); if (!opts.silent) { log.info(`Bundling ${filePath} for preview...`); @@ -1166,7 +1155,7 @@ async function preview( // Handle multiple output files (create tarball) if (out.outputFiles.length > 1) { - const archiveNpm = await import("npm:@ayonli/jsext/archive"); + const archiveNpm = await import("@ayonli/jsext/archive"); if (!opts.silent) { log.info(`Creating tarball for multiple output files...`); } @@ -1177,7 +1166,7 @@ async function preview( tarball.append(new File([mainContent], "main.js", { type: "text/plain" })); for (const file of out.outputFiles) { if (file.path == "/" + mainPath) continue; - // deno-lint-ignore no-explicit-any + const fil = new File([file.contents as any], file.path.substring(1)); tarball.append(fil); } @@ -1185,7 +1174,7 @@ async function preview( isTar = true; } else if (Array.isArray(codebase.assets) && codebase.assets.length > 0) { // Handle assets - const archiveNpm = await import("npm:@ayonli/jsext/archive"); + const archiveNpm = await import("@ayonli/jsext/archive"); if (!opts.silent) { log.info(`Adding assets to tarball...`); } diff --git a/cli/src/commands/sync/global.ts b/cli/src/commands/sync/global.ts index 1b5e975aa6..859dab98b8 100644 --- a/cli/src/commands/sync/global.ts +++ b/cli/src/commands/sync/global.ts @@ -1,4 +1,5 @@ -import { colors, log } from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; let GLOBAL_VERSIONS: { remoteMajor: number | undefined; diff --git a/cli/src/commands/sync/pull.ts b/cli/src/commands/sync/pull.ts index 607d789a6b..54ddfdfa15 100644 --- a/cli/src/commands/sync/pull.ts +++ b/cli/src/commands/sync/pull.ts @@ -1,6 +1,8 @@ -// deno-lint-ignore-file no-explicit-any import { GlobalOptions } from "../../types.ts"; -import { colors, Command, JSZip, log } from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import * as log from "@std/log"; +import JSZip from "jszip"; import { Workspace } from "../workspace/workspace.ts"; import { getHeaders } from "../../utils/utils.ts"; diff --git a/cli/src/commands/sync/push.ts b/cli/src/commands/sync/push.ts index 01f6bea2da..a62150d59e 100644 --- a/cli/src/commands/sync/push.ts +++ b/cli/src/commands/sync/push.ts @@ -1,5 +1,6 @@ -// deno-lint-ignore-file no-explicit-any -import { colors, Command, log } from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import * as log from "@std/log"; import { GlobalOptions } from "../../types.ts"; function stub(_opts: GlobalOptions, _dir?: string) { diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index 41af0159d2..ca5c4ce6d2 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -1,18 +1,16 @@ import { requireLogin } from "../../core/auth.ts"; import { fetchVersion, resolveWorkspace } from "../../core/context.ts"; -import { - colors, - Command, - Confirm, - ensureDir, - JSZip, - log, - minimatch, - path, - SEP, - yamlParseContent, - yamlStringify, -} from "../../../deps.ts"; +import { readFile, writeFile, readdir, stat, rm, copyFile, mkdir } from "node:fs/promises"; +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import { Confirm } from "@cliffy/prompt/confirm"; +import * as log from "@std/log"; +import * as path from "@std/path"; +import { SEPARATOR as SEP } from "@std/path"; +import { stringify as yamlStringify } from "@std/yaml"; +import JSZip from "jszip"; +import { minimatch } from "minimatch"; +import { yamlParseContent } from "../../utils/yaml.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { @@ -178,7 +176,7 @@ async function addCodebaseDigestIfRelevant( let isTs = true; const replacedPath = path.replace(".script.yaml", ".ts"); try { - await Deno.stat(replacedPath); + await stat(replacedPath); } catch { isTs = false; } @@ -231,10 +229,11 @@ export async function FSFSElement( async *getChildren(): AsyncIterable { if (!isDir) return []; try { - for await (const e of Deno.readDir(localP)) { + const entries = await readdir(localP, { withFileTypes: true }); + for (const e of entries) { yield _internal_element( path.join(localP, e.name), - e.isDirectory, + e.isDirectory(), codebases, ); } @@ -242,11 +241,8 @@ export async function FSFSElement( log.warn(`Error reading dir: ${localP}, ${e}`); } }, - // async getContentBytes(): Promise { - // return await Deno.readFile(localP); - // }, async getContentText(): Promise { - const content = await Deno.readTextFile(localP); + const content = await readFile(localP, "utf-8"); const itemPath = localP.substring(p.length + 1); const r = await addCodebaseDigestIfRelevant( itemPath, @@ -258,7 +254,7 @@ export async function FSFSElement( }, }; } - return _internal_element(p, (await Deno.stat(p)).isDirectory, codebases); + return _internal_element(p, (await stat(p)).isDirectory(), codebases); } function prioritizeName(name: string): string { @@ -573,7 +569,6 @@ function ZipFSElement( isDirectory: false, path: path.join(finalPath, s.path), async *getChildren() {}, - // deno-lint-ignore require-await async getContentText() { return s.content; }, @@ -584,7 +579,6 @@ function ZipFSElement( isDirectory: false, path: path.join(finalPath, "flow.yaml"), async *getChildren() {}, - // deno-lint-ignore require-await async getContentText() { return yamlStringify(flow, yamlOptions); }, @@ -618,7 +612,6 @@ function ZipFSElement( isDirectory: false, path: path.join(finalPath, s.path), async *getChildren() {}, - // deno-lint-ignore require-await async getContentText() { return s.content; }, @@ -633,7 +626,6 @@ function ZipFSElement( isDirectory: false, path: path.join(finalPath, "app.yaml"), async *getChildren() {}, - // deno-lint-ignore require-await async getContentText() { return yamlStringify(app, yamlOptions); }, @@ -690,8 +682,7 @@ function ZipFSElement( isDirectory: false, path: path.join(finalPath, filePath.substring(1)), async *getChildren() {}, - // deno-lint-ignore require-await - async getContentText() { + async getContentText() { if (typeof content !== "string") { throw new Error( `Content of raw app file ${filePath} is not a string`, @@ -712,7 +703,6 @@ function ZipFSElement( isDirectory: false, path: path.join(finalPath, APP_BACKEND_FOLDER, s.path), async *getChildren() {}, - // deno-lint-ignore require-await async getContentText() { return s.content; }, @@ -792,7 +782,6 @@ function ZipFSElement( `${runnableId}.yaml`, ), async *getChildren() {}, - // deno-lint-ignore require-await async getContentText() { return yamlStringify(simplifiedRunnable, yamlOptions); }, @@ -813,7 +802,6 @@ function ZipFSElement( isDirectory: false, path: path.join(finalPath, "raw_app.yaml"), async *getChildren() {}, - // deno-lint-ignore require-await async getContentText() { return yamlStringify(rawApp, yamlOptions); }, @@ -824,7 +812,6 @@ function ZipFSElement( isDirectory: false, path: path.join(finalPath, "DATATABLES.md"), async *getChildren() {}, - // deno-lint-ignore require-await async getContentText() { return generateDatatablesDocumentation(data); }, @@ -917,7 +904,6 @@ function ZipFSElement( isDirectory: false, path: removeSuffix(finalPath, ".json") + ".lock", async *getChildren() {}, - // deno-lint-ignore require-await async getContentText() { return lock; }, @@ -946,7 +932,6 @@ function ZipFSElement( ".resource.file." + formatExtension, async *getChildren() {}, - // deno-lint-ignore require-await async getContentText() { return fileContent; }, @@ -975,11 +960,6 @@ function ZipFSElement( } } }, - // // deno-lint-ignore require-await - // async getContentBytes(): Promise { - // throw new Error("Cannot get content of folder"); - // }, - // deno-lint-ignore require-await async getContentText(): Promise { throw new Error("Cannot get content of folder"); }, @@ -1580,7 +1560,7 @@ export async function ignoreF(wmillconf: { } try { - await Deno.stat(".wmillignore"); + await stat(".wmillignore"); throw Error(".wmillignore is not supported anymore, switch to wmill.yaml"); } catch { //expected @@ -1636,7 +1616,6 @@ interface ChangeTracker { rawApps: string[]; } -// deno-lint-ignore no-inner-declarations async function addToChangedIfNotExists(p: string, tracker: ChangeTracker) { const isScript = exts.some((e) => p.endsWith(e)); if (isScript) { @@ -1700,13 +1679,13 @@ export async function pull( } catch (error) { if (error instanceof Error && error.message.includes("overrides")) { log.error(error.message); - Deno.exit(1); + process.exit(1); } throw error; } if (opts.stateful) { - await ensureDir(path.join(Deno.cwd(), ".wmill")); + await mkdir(path.join(process.cwd(), ".wmill"), { recursive: true }); } const workspace = await resolveWorkspace(opts, opts.branch); @@ -1769,8 +1748,8 @@ export async function pull( ); const local = !opts.stateful - ? await FSFSElement(Deno.cwd(), codebases, true) - : await FSFSElement(path.join(Deno.cwd(), ".wmill"), [], true); + ? await FSFSElement(process.cwd(), codebases, true) + : await FSFSElement(path.join(process.cwd(), ".wmill"), [], true); const changes = await compareDynFSElement( remote, @@ -1852,12 +1831,12 @@ export async function pull( } } - const target = path.join(Deno.cwd(), targetPath); - const stateTarget = path.join(Deno.cwd(), ".wmill", targetPath); + const target = path.join(process.cwd(), targetPath); + const stateTarget = path.join(process.cwd(), ".wmill", targetPath); if (change.name === "edited") { if (opts.stateful) { try { - const currentLocal = await Deno.readTextFile(target); + const currentLocal = await readFile(target, "utf-8"); if ( currentLocal !== change.before && currentLocal !== change.after @@ -1915,16 +1894,16 @@ export async function pull( }`, ); } - await Deno.writeTextFile(target, change.after); + await writeFile(target, change.after, "utf-8"); if (opts.stateful) { - await ensureDir(path.dirname(stateTarget)); - await Deno.copyFile(target, stateTarget); + await mkdir(path.dirname(stateTarget), { recursive: true }); + await copyFile(target, stateTarget); } } else if (change.name === "added") { - await ensureDir(path.dirname(target)); + await mkdir(path.dirname(target), { recursive: true }); if (opts.stateful) { - await ensureDir(path.dirname(stateTarget)); + await mkdir(path.dirname(stateTarget), { recursive: true }); log.info( `Adding ${getTypeStrFromPath(change.path)} ${targetPath}${ targetPath !== change.path @@ -1933,7 +1912,7 @@ export async function pull( }`, ); } - await Deno.writeTextFile(target, change.content); + await writeFile(target, change.content, "utf-8"); log.info( `Writing ${getTypeStrFromPath(change.path)} ${targetPath}${ targetPath !== change.path @@ -1942,20 +1921,20 @@ export async function pull( }`, ); if (opts.stateful) { - await Deno.copyFile(target, stateTarget); + await copyFile(target, stateTarget); } } else if (change.name === "deleted") { try { log.info( `Deleting ${getTypeStrFromPath(change.path)} ${change.path}`, ); - await Deno.remove(target); + await rm(target); if (opts.stateful) { - await Deno.remove(stateTarget); + await rm(stateTarget); } } catch { if (opts.stateful) { - await Deno.remove(stateTarget); + await rm(stateTarget); } } } @@ -1973,7 +1952,7 @@ export async function pull( - pushing the changes with \`wmill push --skip-pull\` to override wmill with all your local changes `), ); - Deno.exit(1); + process.exit(1); } } log.info("All local changes pulled, now updating wmill-lock.yaml"); @@ -2189,7 +2168,7 @@ export async function push( } catch (error) { if (error instanceof Error && error.message.includes("overrides")) { log.error(error.message); - Deno.exit(1); + process.exit(1); } throw error; } @@ -2217,7 +2196,7 @@ export async function push( printReport(lintReport, !!opts.jsonOutput); if (!lintReport.success) { log.error(colors.red("Push aborted due to lint failures.")); - Deno.exit(1); + process.exit(1); } } @@ -2292,7 +2271,7 @@ export async function push( false, ); - const local = await FSFSElement(path.join(Deno.cwd(), ""), codebases, false); + const local = await FSFSElement(path.join(process.cwd(), ""), codebases, false); const changes = await compareDynFSElement( local, remote, @@ -2463,7 +2442,7 @@ export async function push( let stateful = opts.stateful; if (stateful) { try { - await Deno.stat(path.join(Deno.cwd(), ".wmill")); + await stat(path.join(process.cwd(), ".wmill")); } catch { stateful = false; } @@ -2526,8 +2505,8 @@ export async function push( let stateTarget = undefined; if (stateful) { try { - stateTarget = path.join(Deno.cwd(), ".wmill", change.path); - await Deno.stat(stateTarget); + stateTarget = path.join(process.cwd(), ".wmill", change.path); + await stat(stateTarget); } catch { stateTarget = undefined; } @@ -2546,7 +2525,7 @@ export async function push( ) ) { if (stateTarget) { - await Deno.writeTextFile(stateTarget, change.after); + await writeFile(stateTarget, change.after, "utf-8"); } continue; } else if ( @@ -2561,12 +2540,12 @@ export async function push( ) ) { if (stateTarget) { - await Deno.writeTextFile(stateTarget, change.after); + await writeFile(stateTarget, change.after, "utf-8"); } continue; } if (stateTarget) { - await ensureDir(path.dirname(stateTarget)); + await mkdir(path.dirname(stateTarget), { recursive: true }); log.info( `Editing ${getTypeStrFromPath(change.path)} ${change.path}`, ); @@ -2579,7 +2558,7 @@ export async function push( const newObj = parseFromPath( resourceFilePath, - await Deno.readTextFile(resourceFilePath), + await readFile(resourceFilePath, "utf-8"), ); // For branch-specific resources, push to the base path on the workspace server @@ -2602,7 +2581,7 @@ export async function push( resourceFilePath, ); if (stateTarget) { - await Deno.writeTextFile(stateTarget, change.after); + await writeFile(stateTarget, change.after, "utf-8"); } continue; } @@ -2632,7 +2611,7 @@ export async function push( ); if (stateTarget) { - await Deno.writeTextFile(stateTarget, change.after); + await writeFile(stateTarget, change.after, "utf-8"); } } else if (change.name === "added") { if ( @@ -2656,7 +2635,7 @@ export async function push( continue; } if (stateTarget) { - await ensureDir(path.dirname(stateTarget)); + await mkdir(path.dirname(stateTarget), { recursive: true }); log.info( `Adding ${getTypeStrFromPath(change.path)} ${change.path}`, ); @@ -2689,7 +2668,7 @@ export async function push( ); if (stateTarget) { - await Deno.writeTextFile(stateTarget, change.content); + await writeFile(stateTarget, change.content, "utf-8"); } } else if (change.name === "deleted") { if (change.path.endsWith(".lock")) { @@ -2754,7 +2733,7 @@ export async function push( let folderExists = false; if (rawAppFolder) { try { - await Deno.stat(rawAppFolder); + await stat(rawAppFolder); folderExists = true; } catch { // folder doesn't exist @@ -2922,7 +2901,7 @@ export async function push( } if (stateTarget) { try { - await Deno.remove(stateTarget); + await rm(stateTarget); } catch { // state target may not exist already } @@ -3051,7 +3030,6 @@ const command = new Command() "--branch ", "Override the current git branch (works even outside a git repository)", ) - // deno-lint-ignore no-explicit-any .action(pull as any) .command("push") .description("Push any local changes and apply them remotely.") @@ -3113,7 +3091,6 @@ const command = new Command() "--locks-required", "Fail if scripts or flow inline scripts that need locks have no locks", ) - // deno-lint-ignore no-explicit-any .action(push as any); export default command; diff --git a/cli/src/commands/trigger/trigger.ts b/cli/src/commands/trigger/trigger.ts index 58bf626523..5e4c8e234a 100644 --- a/cli/src/commands/trigger/trigger.ts +++ b/cli/src/commands/trigger/trigger.ts @@ -1,3 +1,5 @@ +import { stat } from "node:fs/promises"; + import * as wmill from "../../../gen/services.gen.ts"; import { GcpTrigger, @@ -13,7 +15,11 @@ import { NativeTriggerData, NativeServiceName, } from "../../../gen/types.gen.ts"; -import { colors, Command, log, SEP, Table } from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import { Table } from "@cliffy/table"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; import { GlobalOptions, isSuperset, @@ -372,8 +378,8 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) { return; } - const fstat = await Deno.stat(filePath); - if (!fstat.isFile) { + const fstat = await stat(filePath); + if (!fstat.isFile()) { throw new Error("file path must refer to a file."); } diff --git a/cli/src/commands/user/user.ts b/cli/src/commands/user/user.ts index 93ab9236c5..f5d8891c9c 100644 --- a/cli/src/commands/user/user.ts +++ b/cli/src/commands/user/user.ts @@ -1,4 +1,5 @@ -// deno-lint-ignore-file no-explicit-any +import { writeFile } from "node:fs/promises"; + import { requireLogin } from "../../core/auth.ts"; import { GlobalOptions, @@ -7,14 +8,12 @@ import { removePathPrefix, } from "../../types.ts"; import { compareInstanceObjects, InstanceSyncOptions } from "../instance/instance.ts"; -import { - colors, - Command, - log, - Table, - yamlStringify, - yamlParseFile, -} from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import { Table } from "@cliffy/table"; +import * as log from "@std/log"; +import { stringify as yamlStringify } from "@std/yaml"; +import { yamlParseFile } from "../../utils/yaml.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { ExportedInstanceGroup, @@ -417,9 +416,10 @@ export async function pullInstanceUsers( return compareInstanceObjects(remoteUsers, localUsers, "email", "user"); } else { log.info("Pulling users from instance..."); - await Deno.writeTextFile( + await writeFile( instanceUsersPath, - yamlStringify(remoteUsers as any) + yamlStringify(remoteUsers as any), + "utf-8" ); log.info(colors.green(`Users written to ${instanceUsersPath}`)); } @@ -486,9 +486,10 @@ export async function pullInstanceGroups( } else { log.info("Pulling groups from instance..."); - await Deno.writeTextFile( + await writeFile( instanceGroupsPath, - yamlStringify(remoteGroups as any) + yamlStringify(remoteGroups as any), + "utf-8" ); log.info(colors.green(`Groups written to ${instanceGroupsPath}`)); diff --git a/cli/src/commands/variable/variable.ts b/cli/src/commands/variable/variable.ts index 60a4f7320d..1fc831bf87 100644 --- a/cli/src/commands/variable/variable.ts +++ b/cli/src/commands/variable/variable.ts @@ -1,4 +1,5 @@ -// deno-lint-ignore-file no-explicit-any +import { stat } from "node:fs/promises"; + import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace, validatePath } from "../../core/context.ts"; import { @@ -7,7 +8,12 @@ import { parseFromFile, removeType, } from "../../types.ts"; -import { colors, Command, Confirm, log, SEP, Table } from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import { Table } from "@cliffy/table"; +import { colors } from "@cliffy/ansi/colors"; +import { Confirm } from "@cliffy/prompt/confirm"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; import * as wmill from "../../../gen/services.gen.ts"; import { ListableVariable } from "../../../gen/types.gen.ts"; @@ -108,8 +114,8 @@ async function push( return; } - const fstat = await Deno.stat(filePath); - if (!fstat.isFile) { + const fstat = await stat(filePath); + if (!fstat.isFile()) { throw new Error("file path must refer to a file."); } diff --git a/cli/src/commands/worker-groups/worker-groups.ts b/cli/src/commands/worker-groups/worker-groups.ts index a683021bc0..a49769b706 100644 --- a/cli/src/commands/worker-groups/worker-groups.ts +++ b/cli/src/commands/worker-groups/worker-groups.ts @@ -1,6 +1,8 @@ -import { Command, Confirm, setClient, Table } from "../../../deps.ts"; - -import { log } from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import { Table } from "@cliffy/table"; +import { Confirm } from "@cliffy/prompt/confirm"; +import * as log from "@std/log"; +import { setClient } from "../../core/client.ts"; import { allInstances, getActiveInstance, InstanceSyncOptions, pickInstance } from "../instance/instance.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { pullInstanceConfigs, pushInstanceConfigs } from "../../core/settings.ts"; diff --git a/cli/src/commands/workers/workers.ts b/cli/src/commands/workers/workers.ts index f3d00ff63f..70decb109a 100644 --- a/cli/src/commands/workers/workers.ts +++ b/cli/src/commands/workers/workers.ts @@ -1,5 +1,6 @@ -import { Command, Table } from "../../../deps.ts"; -import { log } from "../../../deps.ts"; +import { Command } from "@cliffy/command"; +import { Table } from "@cliffy/table"; +import * as log from "@std/log"; import * as wmill from "../../../gen/services.gen.ts"; import { pickInstance } from "../instance/instance.ts"; diff --git a/cli/src/commands/workspace/fork.ts b/cli/src/commands/workspace/fork.ts index 0bd3b9eec6..19091d6b95 100644 --- a/cli/src/commands/workspace/fork.ts +++ b/cli/src/commands/workspace/fork.ts @@ -1,6 +1,8 @@ -// deno-lint-ignore-file no-explicit-any import { GlobalOptions } from "../../types.ts"; -import { colors, Input, log, setClient } from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import { Input } from "@cliffy/prompt/input"; +import * as log from "@std/log"; +import { setClient } from "../../core/client.ts"; import { allWorkspaces, list, removeWorkspace } from "./workspace.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { getCurrentGitBranch, getOriginalBranchForWorkspaceForks, isGitRepository } from "../../utils/git.ts"; @@ -159,7 +161,7 @@ async function deleteWorkspaceFork( } if (!opts.yes) { - const { Select } = await import("../../../deps.ts"); + const { Select } = await import("@cliffy/prompt/select"); const choice = await Select.prompt({ message: `Are you sure you want to delete the forked workspace with id: \`${workspace.workspaceId}\`? This action will delete the workspace `, options: [ diff --git a/cli/src/commands/workspace/workspace.ts b/cli/src/commands/workspace/workspace.ts index a4d4d224ff..b7464e7cfd 100644 --- a/cli/src/commands/workspace/workspace.ts +++ b/cli/src/commands/workspace/workspace.ts @@ -1,19 +1,18 @@ -// deno-lint-ignore-file no-explicit-any +import { readFile, writeFile, open as fsOpen } from "node:fs/promises"; +import process from "node:process"; import { GlobalOptions } from "../../types.ts"; import { getActiveWorkspaceConfigFilePath, getWorkspaceConfigFilePath, } from "../../../windmill-utils-internal/src/config/config.ts"; import { loginInteractive, tryGetLoginInfo } from "../../core/login.ts"; -import { - colors, - Command, - Confirm, - Input, - log, - setClient, - Table, -} from "../../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import { Command } from "@cliffy/command"; +import { Confirm } from "@cliffy/prompt/confirm"; +import { Input } from "@cliffy/prompt/input"; +import { Table } from "@cliffy/table"; +import * as log from "@std/log"; +import { setClient } from "../../core/client.ts"; import { requireLogin } from "../../core/auth.ts"; import { createWorkspaceFork, deleteWorkspaceFork } from "./fork.ts"; @@ -31,7 +30,7 @@ export async function allWorkspaces( ): Promise { try { const file = await getWorkspaceConfigFilePath(configDirOverride); - const txt = await Deno.readTextFile(file); + const txt = await readFile(file, "utf-8"); return txt .split("\n") .map((line) => { @@ -55,7 +54,7 @@ async function getActiveWorkspaceName( } try { const file = await getActiveWorkspaceConfigFilePath(opts?.configDir); - return await Deno.readTextFile(file); + return await readFile(file, "utf-8"); } catch { return undefined; } @@ -146,7 +145,7 @@ export async function setActiveWorkspace( configDirOverride?: string ) { const file = await getActiveWorkspaceConfigFilePath(configDirOverride); - await Deno.writeTextFile(file, workspaceName); + await writeFile(file, workspaceName, "utf-8"); } export async function add( @@ -202,7 +201,7 @@ export async function add( remote = new URL(remote).toString(); // add trailing slash in all cases! let token = await tryGetLoginInfo(opts); - if (!token && Deno.stdin.isTerminal && !Deno.stdin.isTerminal()) { + if (!token && !(process.stdin.isTTY ?? false)) { log.info("Not a TTY, can't login interactively. Pass the token in --token"); return; } @@ -257,7 +256,7 @@ export async function add( for (const workspace of workspaces) { log.info(`- ${workspace.id} (name: ${workspace.name})`); } - Deno.exit(1); + process.exit(1); } const added = await addWorkspace( @@ -287,7 +286,7 @@ export async function addWorkspace(workspace: Workspace, opts: any): Promise + w.remote === workspace.remote && + w.workspaceId === workspace.workspaceId && + w.name !== workspace.name + ); + if (backendConflict) { + if (opts.force) { + // Remove the conflicting workspace before adding the new one + await removeWorkspace(backendConflict.name, true, opts); + } else { + throw new Error( + `Backend constraint violation: (${workspace.remote}, ${workspace.workspaceId}) already exists as "${backendConflict.name}". Use --force to overwrite.` + ); + } + } + // Remove existing workspace with same name (if updating) await removeWorkspace(workspace.name, true, opts); // Add the new workspace const filePath = await getWorkspaceConfigFilePath(opts.configDir); - const file = await Deno.open(filePath, { - append: true, - write: true, - read: true, - create: true, - }); - await file.write(new TextEncoder().encode(JSON.stringify(workspace) + "\n")); - - file.close(); + const fh = await fsOpen(filePath, "a"); + await fh.write(JSON.stringify(workspace) + "\n"); + await fh.close(); return true; } @@ -377,12 +388,13 @@ export async function removeWorkspace( } const filePath = await getWorkspaceConfigFilePath(opts.configDir); - await Deno.writeTextFile( + await writeFile( filePath, orgWorkspaces .filter((x) => x.name !== name) .map((x) => JSON.stringify(x)) - .join("\n") + "\n" + .join("\n") + "\n", + "utf-8" ); if (!silent) { @@ -506,9 +518,9 @@ async function bind( } // Write back the updated config - const { yamlStringify } = await import("../../../deps.ts"); + const { stringify: yamlStringify } = await import("@std/yaml"); try { - await Deno.writeTextFile("wmill.yaml", yamlStringify(config)); + await writeFile("wmill.yaml", yamlStringify(config), "utf-8"); } catch (error) { log.error(colors.red(`Failed to save configuration: ${(error as Error).message}`)); return; diff --git a/cli/src/core/auth.ts b/cli/src/core/auth.ts index 2831761141..311fe16ea7 100644 --- a/cli/src/core/auth.ts +++ b/cli/src/core/auth.ts @@ -1,5 +1,6 @@ -// deno-lint-ignore-file no-explicit-any -import { colors, log, setClient } from "../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { setClient } from "./client.ts"; import * as wmill from "../../gen/services.gen.ts"; import { GlobalUserInfo } from "../../gen/types.gen.ts"; diff --git a/cli/src/core/branch-profiles.ts b/cli/src/core/branch-profiles.ts index 006b7fed18..8b1c52f9d5 100644 --- a/cli/src/core/branch-profiles.ts +++ b/cli/src/core/branch-profiles.ts @@ -1,4 +1,5 @@ -import { log } from "../../deps.ts"; +import * as log from "@std/log"; +import { readFile, writeFile } from "node:fs/promises"; import { getStore } from "./store.ts"; export interface BranchProfileMapping { @@ -16,7 +17,7 @@ export async function getBranchProfilesPath(configDirOverride?: string): Promise export async function loadBranchProfiles(configDirOverride?: string): Promise { try { const path = await getBranchProfilesPath(configDirOverride); - const content = await Deno.readTextFile(path); + const content = await readFile(path, "utf-8"); return JSON.parse(content); } catch { // File doesn't exist or invalid JSON - return empty mapping @@ -29,7 +30,7 @@ export async function saveBranchProfiles( configDirOverride?: string ): Promise { const path = await getBranchProfilesPath(configDirOverride); - await Deno.writeTextFile(path, JSON.stringify(mapping, null, 2)); + await writeFile(path, JSON.stringify(mapping, null, 2), "utf-8"); } export function getBranchProfileKey( diff --git a/cli/src/core/client.ts b/cli/src/core/client.ts new file mode 100644 index 0000000000..4b0b98e30f --- /dev/null +++ b/cli/src/core/client.ts @@ -0,0 +1,15 @@ +import { OpenAPI } from "../../gen/index.ts"; + +export function setClient(token?: string, baseUrl?: string) { + if (baseUrl === undefined) { + baseUrl = process.env["BASE_INTERNAL_URL"] ?? + process.env["BASE_URL"] ?? + "http://localhost:8000"; + } + if (token === undefined) { + token = process.env["WM_TOKEN"] ?? "no_token"; + } + OpenAPI.WITH_CREDENTIALS = true; + OpenAPI.TOKEN = token; + OpenAPI.BASE = baseUrl + "/api"; +} diff --git a/cli/src/core/conf.ts b/cli/src/core/conf.ts index ce6c4f5bf1..775f71befb 100644 --- a/cli/src/core/conf.ts +++ b/cli/src/core/conf.ts @@ -1,4 +1,7 @@ -import { log, yamlParseFile, Confirm, yamlStringify } from "../../deps.ts"; +import * as log from "@std/log"; +import { yamlParseFile } from "../utils/yaml.ts"; +import { Confirm } from "@cliffy/prompt/confirm"; +import { stringify as yamlStringify } from "@std/yaml"; import { getCurrentGitBranch, getOriginalBranchForWorkspaceForks, @@ -6,6 +9,7 @@ import { } from "../utils/git.ts"; import { join, dirname, resolve, relative } from "node:path"; import { existsSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; import { execSync } from "node:child_process"; import { setNonDottedPaths } from "../utils/resource_folders.ts"; @@ -133,7 +137,7 @@ function getGitRepoRoot(): string | null { export const GLOBAL_CONFIG_OPT = { noCdToRoot: false }; function findWmillYaml(): string | null { - const startDir = resolve(Deno.cwd()); + const startDir = resolve(process.cwd()); const isInGitRepo = isGitRepository(); const gitRoot = isInGitRepo ? getGitRepoRoot() : null; @@ -174,7 +178,7 @@ function findWmillYaml(): string | null { log.warn(`⚠️ wmill.yaml found in parent directory: ${relativePath}`); // Change working directory to where wmill.yaml was found - Deno.chdir(configDir); + process.chdir(configDir); log.info(`📁 Changed working directory to: ${configDir}`); } @@ -251,7 +255,7 @@ export async function readConfigFile(): Promise { // Perform single atomic write if any migrations are needed if (needsConfigWrite) { try { - await Deno.writeTextFile(wmillYamlPath, yamlStringify(conf)); + await writeFile(wmillYamlPath, yamlStringify(conf), "utf-8"); // Log all migration messages after successful write migrationMessages.forEach((msg) => { if (msg.startsWith("⚠️")) { @@ -418,7 +422,7 @@ export async function validateBranchConfiguration( // Current branch must be defined in gitBranches config if (currentBranch && !gitBranches[currentBranch]) { // In interactive mode, offer to create the branch - if (Deno.stdin.isTerminal()) { + if (!!process.stdin.isTTY) { const availableBranches = Object.keys(gitBranches).join(", "); log.info( `Current Git branch '${currentBranch}' is not defined in the gitBranches configuration.\n` + @@ -458,7 +462,7 @@ export async function validateBranchConfiguration( } currentConfig.gitBranches[currentBranch] = { overrides: {} }; - await Deno.writeTextFile("wmill.yaml", yamlStringify(currentConfig)); + await writeFile("wmill.yaml", yamlStringify(currentConfig), "utf-8"); log.info( `✅ Created empty branch configuration for '${currentBranch}'` diff --git a/cli/src/core/context.ts b/cli/src/core/context.ts index 714b613cdd..0e71628599 100644 --- a/cli/src/core/context.ts +++ b/cli/src/core/context.ts @@ -1,5 +1,8 @@ -// deno-lint-ignore-file no-explicit-any -import { colors, log, Select, Confirm, Input } from "../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { Select } from "@cliffy/prompt/select"; +import { Confirm } from "@cliffy/prompt/confirm"; +import { Input } from "@cliffy/prompt/input"; import { loginInteractive } from "./login.ts"; import { GlobalOptions } from "../types.ts"; @@ -56,7 +59,7 @@ async function selectFromMultipleProfiles( } // No last used or it no longer exists - prompt for selection - if (!Deno.stdin.isTerminal() || !Deno.stdout.isTerminal()) { + if (!!!process.stdin.isTTY || !!!process.stdout.isTTY) { const selectedProfile = profiles[0]; log.info( colors.yellow( @@ -129,7 +132,7 @@ async function createWorkspaceProfileInteractively( ); } - if (!Deno.stdin.isTerminal() || !Deno.stdout.isTerminal()) { + if (!!!process.stdin.isTTY || !!!process.stdout.isTTY) { log.info( "Not a TTY, cannot create profile interactively. Use 'wmill workspace add' first." ); @@ -382,7 +385,7 @@ export async function resolveWorkspace( normalizedBaseUrl = new URL(opts.baseUrl).toString(); // add trailing slash if not present } catch (error) { log.info(colors.red(`Invalid base URL: ${opts.baseUrl}`)); - return Deno.exit(-1); + return process.exit(-1); } // Try to find existing workspace profile by name, then by workspaceId + remote @@ -423,7 +426,7 @@ export async function resolveWorkspace( `Base URL mismatch: --base-url is ${normalizedBaseUrl} but workspace profile "${opts.workspace}" uses ${existingWorkspace.remote}` ) ); - return Deno.exit(-1); + return process.exit(-1); } // Use the existing workspace profile (preserves workspace name) return { @@ -446,7 +449,7 @@ export async function resolveWorkspace( "If you specify a base URL with --base-url, you must also specify a workspace (--workspace) and token (--token)." ) ); - return Deno.exit(-1); + return process.exit(-1); } } @@ -479,7 +482,7 @@ export async function resolveWorkspace( `Failed to resolve workspace profile for workspace fork. This most likely means that the original branch \`${originalBranch}\` where \`${branch}\` is originally forked from, is not setup in the wmill.yaml. You need to update the \`gitBranches\` section for \`${originalBranch}\` to include workspaceId and baseUrl.` ) ); - return Deno.exit(-1); + return process.exit(-1); } } @@ -492,7 +495,7 @@ export async function resolveWorkspace( // If everything failed, show error log.info(colors.red.bold("No workspace given and no default set.")); - return Deno.exit(-1); + return process.exit(-1); } export async function fetchVersion(baseUrl: string): Promise { diff --git a/cli/src/core/login.ts b/cli/src/core/login.ts index 6da90b8042..c492347c0f 100644 --- a/cli/src/core/login.ts +++ b/cli/src/core/login.ts @@ -1,10 +1,15 @@ import { GlobalOptions } from "../types.ts"; -import { colors, getPort, log, open, Secret, Select } from "../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import * as getPort from "get-port"; +import * as log from "@std/log"; +import * as open from "open"; +import { Secret } from "@cliffy/prompt/secret"; +import { Select } from "@cliffy/prompt/select"; import * as http from "node:http"; export async function loginInteractive(remote: string) { let token: string | undefined; - if (Deno.stdin.isTerminal && !Deno.stdin.isTerminal()) { + if (!process.stdin.isTTY) { log.info("Not a TTY, can't login interactively."); return undefined; } @@ -30,7 +35,6 @@ export async function loginInteractive(remote: string) { return token; } -// deno-lint-ignore require-await export async function tryGetLoginInfo( opts: GlobalOptions ): Promise { @@ -45,8 +49,8 @@ export async function browserLogin( baseUrl: string ): Promise { const env = - Deno.env.get("TOKEN_PORT") != undefined - ? parseInt(Deno.env.get("TOKEN_PORT")!) + process.env["TOKEN_PORT"] != undefined + ? parseInt(process.env["TOKEN_PORT"]!) : undefined; const port = await getPort.default({ port: env }); @@ -55,32 +59,6 @@ export async function browserLogin( return undefined; } - // const server = Deno.listen({ transport: "tcp", port }); - // const url = `${baseUrl}user/cli?port=${port}`; - // log.info(`Login by going to ${url}`); - // try { - // await open.openApp(open.apps.browser, { arguments: [url] }); - - // log.info("Opened browser for you"); - // } catch { - // console.error(`Failed to open browser, please navigate to ${url}`); - // } - // const firstConnection = await server.accept(); - // const httpFirstConnection = Deno.serveHttp(firstConnection); - // const firstRequest = (await httpFirstConnection.nextRequest())!; - // const params = new URL(firstRequest.request.url!).searchParams; - // const token = params.get("token"); - // // const _workspace = params.get("workspace"); - // await firstRequest?.respondWith( - // Response.redirect(baseUrl + "user/cli-success", 302) - // ); - - // setTimeout(() => { - // httpFirstConnection.close(); - // server.close(); - // }, 10); - // return token ?? undefined; - return new Promise((resolve) => { const server = http.createServer((req, res) => { const params = new URL(req.url!, `http://${req.headers.host}`) diff --git a/cli/src/core/settings.ts b/cli/src/core/settings.ts index ed83f60658..7075ba2edc 100644 --- a/cli/src/core/settings.ts +++ b/cli/src/core/settings.ts @@ -1,5 +1,10 @@ import process from "node:process"; -import { colors, Confirm, log, yamlParseFile, yamlStringify } from "../../deps.ts"; +import { writeFile } from "node:fs/promises"; +import { colors } from "@cliffy/ansi/colors"; +import { Confirm } from "@cliffy/prompt/confirm"; +import * as log from "@std/log"; +import { yamlParseFile } from "../utils/yaml.ts"; +import { stringify as yamlStringify } from "@std/yaml"; import * as wmill from "../../gen/services.gen.ts"; import { AIConfig, Config, GlobalSetting } from "../../gen/types.gen.ts"; import { compareInstanceObjects, InstanceSyncOptions } from "../commands/instance/instance.ts"; @@ -493,9 +498,10 @@ export async function pullInstanceSettings( remoteSettings, "encode" ); - await Deno.writeTextFile( + await writeFile( instanceSettingsPath, - yamlStringify(processedSettings) + yamlStringify(processedSettings), + "utf-8" ); log.info(colors.green(`Settings written to ${instanceSettingsPath}`)); @@ -602,9 +608,10 @@ export async function pullInstanceConfigs( } else { log.info("Pulling configs from instance"); - await Deno.writeTextFile( + await writeFile( instanceConfigsPath, - yamlStringify(remoteConfigs as any) + yamlStringify(remoteConfigs as any), + "utf-8" ); log.info(colors.green(`Configs written to ${instanceConfigsPath}`)); diff --git a/cli/src/core/specific_items.ts b/cli/src/core/specific_items.ts index e0484fb53a..aa3f6652fc 100644 --- a/cli/src/core/specific_items.ts +++ b/cli/src/core/specific_items.ts @@ -1,4 +1,4 @@ -import { minimatch } from "../../deps.ts"; +import { minimatch } from "minimatch"; import { getCurrentGitBranch, isGitRepository } from "../utils/git.ts"; import { isFileResource } from "../utils/utils.ts"; import { SyncOptions } from "./conf.ts"; diff --git a/cli/src/core/store.ts b/cli/src/core/store.ts index 5843b6ad02..cc58ca023f 100644 --- a/cli/src/core/store.ts +++ b/cli/src/core/store.ts @@ -1,4 +1,4 @@ -import { ensureDir } from "../../deps.ts"; +import { mkdir } from "node:fs/promises"; import { getConfigDirPath } from "../../windmill-utils-internal/src/config/config.ts"; function hash_string(str: string): number { @@ -17,6 +17,6 @@ function hash_string(str: string): number { export async function getStore(baseUrl: string, configDirOverride?: string): Promise { const baseHash = Math.abs(hash_string(baseUrl)).toString(16); const baseStore = (await getConfigDirPath(configDirOverride)) + baseHash + "/"; - await ensureDir(baseStore); + await mkdir(baseStore, { recursive: true }); return baseStore; } \ No newline at end of file diff --git a/cli/src/main.ts b/cli/src/main.ts index 2a1c9f6b93..e7ace077c6 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -1,16 +1,9 @@ -import { - Command, - CompletionsCommand, - UpgradeCommand, - esMain, - log, -} from "../deps.ts"; +import { Command } from "@cliffy/command"; +import { CompletionsCommand } from "@cliffy/command/completions"; +import { UpgradeCommand } from "@cliffy/command/upgrade"; +import * as log from "@std/log"; -// Node.js-specific imports for symlink resolution in isMain() -// These are only used in Node.js, not Deno -// dnt-shim-ignore import { realpathSync } from "node:fs"; -// dnt-shim-ignore import { fileURLToPath } from "node:url"; import flow from "./commands/flow/flow.ts"; import app from "./commands/app/app.ts"; @@ -72,13 +65,6 @@ export { workspaceAdd, }; -// addEventListener("error", (event) => { -// if (event.error) { -// console.error("Error details of: " + event.error.message); -// console.error(JSON.stringify(event.error, null, 4)); -// } -// }); - export const VERSION = "1.641.0"; // Re-exported from constants.ts to maintain backwards compatibility @@ -165,7 +151,9 @@ const command = new Command() const backendVersion = await fetchVersion(workspace.remote); console.log("Backend Version: " + backendVersion); } catch (e) { - console.warn("Cannot fetch backend version: " + e); + console.warn( + `Cannot fetch backend version from ${workspace.remote} (workspace: ${workspace.name}): ${e}` + ); } } else { console.warn( @@ -188,15 +176,16 @@ const command = new Command() async function main() { try { - if (Deno.args.length === 0) { + const args = process.argv.slice(2); + if (args.length === 0) { command.showHelp(); } const LOG_LEVEL = - Deno.args.includes("--verbose") || Deno.args.includes("--debug") + args.includes("--verbose") || args.includes("--debug") ? "DEBUG" : "INFO"; - // const NO_COLORS = Deno.args.includes("--no-colors"); - setShowDiffs(Deno.args.includes("--show-diffs")); + // const NO_COLORS = args.includes("--no-colors"); + setShowDiffs(args.includes("--show-diffs")); const isWin = await getIsWin(); log.setup({ @@ -219,7 +208,7 @@ async function main() { if (extraHeaders) { OpenAPI.HEADERS = extraHeaders; } - await command.parse(Deno.args); + await command.parse(args); } catch (e) { if (e && typeof e === "object" && "name" in e && e.name === "ApiError") { console.log( @@ -231,41 +220,18 @@ async function main() { } function isMain() { - // dnt-shim-ignore - const { Deno } = globalThis as any; + // Handle symlinks properly: resolve symlinks when comparing process.argv[1] + // with import.meta.url, so `wmill` symlink matches the real file path. + try { + const scriptPath = process.argv[1]; + if (!scriptPath) return false; - const isDeno = Deno != undefined; + const realScriptPath = realpathSync(scriptPath); + const modulePath = fileURLToPath(import.meta.url); - if (isDeno) { - const isMain = import.meta.main; - if (isMain) { - if (!Deno.args.includes("completions")) { - if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") { - log.warn( - "Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true" - ); - } - } - } - return isMain; - } else { - // For Node.js, we need to handle symlinks properly. - // The dnt polyfill doesn't resolve symlinks when comparing process.argv[1] - // with import.meta.url, so `wmill` symlink doesn't match the real file path. - // We resolve symlinks manually to get accurate comparison. - try { - const scriptPath = process.argv[1]; - if (!scriptPath) return false; - - const realScriptPath = realpathSync(scriptPath); - const modulePath = fileURLToPath(import.meta.url); - - return realScriptPath === modulePath; - } catch { - // Fallback to esMain if something fails - //@ts-ignore - return esMain.default(import.meta); - } + return realScriptPath === modulePath; + } catch { + return false; } } if (isMain()) { diff --git a/cli/src/types.ts b/cli/src/types.ts index 22cc1a2b33..7115f743ff 100644 --- a/cli/src/types.ts +++ b/cli/src/types.ts @@ -1,14 +1,11 @@ -// deno-lint-ignore-file no-explicit-any - -import { - colors, - Diff, - log, - path, - SEP, - yamlParseContent, - yamlStringify, -} from "../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import * as Diff from "diff"; +import * as log from "@std/log"; +import * as path from "@std/path"; +import { SEPARATOR as SEP } from "@std/path"; +import { stringify as yamlStringify } from "@std/yaml"; +import { yamlParseContent } from "./utils/yaml.ts"; +import { readFileSync } from "node:fs"; import { pushApp } from "./commands/app/app.ts"; import { pushFolder } from "./commands/folder/folder.ts"; import { pushFlow } from "./commands/flow/flow.ts"; @@ -228,9 +225,9 @@ export function parseFromPath(p: string, content: string): any { } export function parseFromFile(p: string): any { if (p.endsWith(".json")) { - return JSON.parse(Deno.readTextFileSync(p)); + return JSON.parse(readFileSync(p, "utf-8")); } else if (p.endsWith(".yaml") || p.endsWith(".yml")) { - return yamlParseContent(p, Deno.readTextFileSync(p)); + return yamlParseContent(p, readFileSync(p, "utf-8")); } else { throw new Error("Could not read file " + p); } diff --git a/cli/src/utils/codebase.ts b/cli/src/utils/codebase.ts index 84424f87c3..2fdad891d6 100644 --- a/cli/src/utils/codebase.ts +++ b/cli/src/utils/codebase.ts @@ -1,5 +1,5 @@ import { Codebase, SyncOptions } from "../core/conf.ts"; -import { log } from "../../deps.ts"; +import * as log from "@std/log"; import { digestDir } from "./utils.ts"; export type SyncCodebase = Codebase & { diff --git a/cli/src/utils/git.ts b/cli/src/utils/git.ts index a67d343408..c402a5906a 100644 --- a/cli/src/utils/git.ts +++ b/cli/src/utils/git.ts @@ -1,4 +1,4 @@ -import { log } from "../../deps.ts"; +import * as log from "@std/log"; import { execSync } from "node:child_process"; import { WM_FORK_PREFIX } from "../core/constants.ts"; diff --git a/cli/src/utils/metadata.ts b/cli/src/utils/metadata.ts index 25ce5fdaa5..7ddba177b3 100644 --- a/cli/src/utils/metadata.ts +++ b/cli/src/utils/metadata.ts @@ -1,6 +1,12 @@ -// deno-lint-ignore-file no-explicit-any import { GlobalOptions } from "../types.ts"; -import { SEP, colors, log, yamlParseFile, yamlStringify } from "../../deps.ts"; +import { SEPARATOR as SEP } from "@std/path"; +import { colors } from "@cliffy/ansi/colors"; +import * as log from "@std/log"; +import { stringify as yamlStringify } from "@std/yaml"; +import { yamlParseFile } from "./yaml.ts"; +import { readFile, writeFile, stat, rm, readdir } from "node:fs/promises"; +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; import { ScriptMetadata, defaultScriptMetadata, @@ -18,6 +24,25 @@ import { SyncCodebase } from "./codebase.ts"; import { argSigToJsonSchemaType } from "../../windmill-utils-internal/src/parse/parse-schema.ts"; import { getIsWin } from "./utils.ts"; +const _require = createRequire(import.meta.url); +const _parserCache = new Map>(); + +function loadParser(pkgName: string): Promise { + let p = _parserCache.get(pkgName); + if (!p) { + p = (async () => { + const mod = await import(pkgName); + const wasmPath = _require.resolve( + `${pkgName}/windmill_parser_wasm_bg.wasm` + ); + await mod.default(readFileSync(wasmPath)); + return mod; + })(); + _parserCache.set(pkgName, p); + } + return p; +} + export class LockfileGenerationError extends Error { constructor(message: string) { super(message); @@ -31,11 +56,12 @@ export async function getRawWorkspaceDependencies(): Promise = {}; try { - for await (const entry of Deno.readDir("dependencies")) { - if (entry.isDirectory) continue; + const entries = await readdir("dependencies", { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) continue; const filePath = `dependencies/${entry.name}`; - const content = await Deno.readTextFile(filePath); + const content = await readFile(filePath, "utf-8"); // Find matching language for (const lang of workspaceDependenciesLanguages) { @@ -120,7 +146,7 @@ export async function filterWorkspaceDependenciesForScripts( if (content.startsWith("!inline ")) { const filePath = folder + sep + content.replace("!inline ", ""); try { - content = await Deno.readTextFile(filePath); + content = await readFile(filePath, "utf-8"); } catch { continue; } @@ -173,8 +199,8 @@ export async function generateScriptMetadataInternal( ); // read script content - const scriptContent = await Deno.readTextFile(scriptPath); - const metadataContent = await Deno.readTextFile(metadataWithType.path); + const scriptContent = await readFile(scriptPath, "utf-8"); + const metadataContent = await readFile(metadataWithType.path, "utf-8"); const filteredRawWorkspaceDependencies = filterWorkspaceDependencies( rawWorkspaceDependencies, @@ -250,7 +276,7 @@ export async function generateScriptMetadataInternal( ); await updateMetadataGlobalLock(remotePath, hash); if (!justUpdateMetadataLock) { - await Deno.writeTextFile(metaPath, newMetadataContent); + await writeFile(metaPath, newMetadataContent, "utf-8"); } return `${remotePath} (${language})`; } @@ -490,12 +516,12 @@ async function updateScriptLock( const lockPath = remotePath + ".script.lock"; if (lock != "") { - await Deno.writeTextFile(lockPath, lock); + await writeFile(lockPath, lock, "utf-8"); metadataContent.lock = "!inline " + lockPath.replaceAll(SEP, "/"); } else { try { - if (await Deno.stat(lockPath)) { - await Deno.remove(lockPath); + if (await stat(lockPath)) { + await rm(lockPath); } } catch (e) { log.info(colors.yellow(`Error removing lock file ${lockPath}: ${e}`)); @@ -519,139 +545,98 @@ export async function inferSchema( }> { let inferedSchema: any; if (language === "python3") { - const { parse_python } = await import( - "../../wasm/py/windmill_parser_wasm.js" - ); + const { parse_python } = await loadParser("windmill-parser-wasm-py"); inferedSchema = JSON.parse(parse_python(content)); } else if (language === "nativets") { - const { parse_deno } = await import( - "../../wasm/ts/windmill_parser_wasm.js" - ); + const { parse_deno } = await loadParser("windmill-parser-wasm-ts"); inferedSchema = JSON.parse(parse_deno(content)); } else if (language === "bun") { - const { parse_deno } = await import( - "../../wasm/ts/windmill_parser_wasm.js" - ); + const { parse_deno } = await loadParser("windmill-parser-wasm-ts"); inferedSchema = JSON.parse(parse_deno(content)); } else if (language === "deno") { - const { parse_deno } = await import( - "../../wasm/ts/windmill_parser_wasm.js" - ); + const { parse_deno } = await loadParser("windmill-parser-wasm-ts"); inferedSchema = JSON.parse(parse_deno(content)); } else if (language === "go") { - const { parse_go } = await import("../../wasm/go/windmill_parser_wasm.js"); + const { parse_go } = await loadParser("windmill-parser-wasm-go"); inferedSchema = JSON.parse(parse_go(content)); } else if (language === "mysql") { - const { parse_mysql } = await import( - "../../wasm/regex/windmill_parser_wasm.js" - ); - + const { parse_mysql } = await loadParser("windmill-parser-wasm-regex"); inferedSchema = JSON.parse(parse_mysql(content)); inferedSchema.args = [ { name: "database", typ: { resource: "mysql" } }, ...inferedSchema.args, ]; } else if (language === "bigquery") { - const { parse_bigquery } = await import( - "../../wasm/regex/windmill_parser_wasm.js" - ); + const { parse_bigquery } = await loadParser("windmill-parser-wasm-regex"); inferedSchema = JSON.parse(parse_bigquery(content)); inferedSchema.args = [ { name: "database", typ: { resource: "bigquery" } }, ...inferedSchema.args, ]; } else if (language === "oracledb") { - const { parse_oracledb } = await import( - "../../wasm/regex/windmill_parser_wasm.js" - ); + const { parse_oracledb } = await loadParser("windmill-parser-wasm-regex"); inferedSchema = JSON.parse(parse_oracledb(content)); inferedSchema.args = [ { name: "database", typ: { resource: "oracledb" } }, ...inferedSchema.args, ]; } else if (language === "snowflake") { - const { parse_snowflake } = await import( - "../../wasm/regex/windmill_parser_wasm.js" - ); + const { parse_snowflake } = await loadParser("windmill-parser-wasm-regex"); inferedSchema = JSON.parse(parse_snowflake(content)); inferedSchema.args = [ { name: "database", typ: { resource: "snowflake" } }, ...inferedSchema.args, ]; } else if (language === "mssql") { - const { parse_mssql } = await import( - "../../wasm/regex/windmill_parser_wasm.js" - ); + const { parse_mssql } = await loadParser("windmill-parser-wasm-regex"); inferedSchema = JSON.parse(parse_mssql(content)); inferedSchema.args = [ { name: "database", typ: { resource: "ms_sql_server" } }, ...inferedSchema.args, ]; } else if (language === "postgresql") { - const { parse_sql } = await import( - "../../wasm/regex/windmill_parser_wasm.js" - ); + const { parse_sql } = await loadParser("windmill-parser-wasm-regex"); inferedSchema = JSON.parse(parse_sql(content)); inferedSchema.args = [ { name: "database", typ: { resource: "postgresql" } }, ...inferedSchema.args, ]; } else if (language === "duckdb") { - const { parse_duckdb } = await import( - "../../wasm/regex/windmill_parser_wasm.js" - ); + const { parse_duckdb } = await loadParser("windmill-parser-wasm-regex"); inferedSchema = JSON.parse(parse_duckdb(content)); } else if (language === "graphql") { - const { parse_graphql } = await import( - "../../wasm/regex/windmill_parser_wasm.js" - ); + const { parse_graphql } = await loadParser("windmill-parser-wasm-regex"); inferedSchema = JSON.parse(parse_graphql(content)); inferedSchema.args = [ { name: "api", typ: { resource: "graphql" } }, ...inferedSchema.args, ]; } else if (language === "bash") { - const { parse_bash } = await import( - "../../wasm/regex/windmill_parser_wasm.js" - ); + const { parse_bash } = await loadParser("windmill-parser-wasm-regex"); inferedSchema = JSON.parse(parse_bash(content)); } else if (language === "powershell") { - const { parse_powershell } = await import( - "../../wasm/regex/windmill_parser_wasm.js" - ); + const { parse_powershell } = await loadParser("windmill-parser-wasm-regex"); inferedSchema = JSON.parse(parse_powershell(content)); } else if (language === "php") { - const { parse_php } = await import( - "../../wasm/php/windmill_parser_wasm.js" - ); + const { parse_php } = await loadParser("windmill-parser-wasm-php"); inferedSchema = JSON.parse(parse_php(content)); } else if (language === "rust") { - const { parse_rust } = await import( - "../../wasm/rust/windmill_parser_wasm.js" - ); + const { parse_rust } = await loadParser("windmill-parser-wasm-rust"); inferedSchema = JSON.parse(parse_rust(content)); } else if (language === "csharp") { - const { parse_csharp } = await import( - "../../wasm/csharp/windmill_parser_wasm.js" - ); + const { parse_csharp } = await loadParser("windmill-parser-wasm-csharp"); inferedSchema = JSON.parse(parse_csharp(content)); } else if (language === "nu") { - const { parse_nu } = await import("../../wasm/nu/windmill_parser_wasm.js"); + const { parse_nu } = await loadParser("windmill-parser-wasm-nu"); inferedSchema = JSON.parse(parse_nu(content)); } else if (language === "ansible") { - const { parse_ansible } = await import( - "../../wasm/yaml/windmill_parser_wasm.js" - ); + const { parse_ansible } = await loadParser("windmill-parser-wasm-yaml"); inferedSchema = JSON.parse(parse_ansible(content)); } else if (language === "java") { - const { parse_java } = await import( - "../../wasm/java/windmill_parser_wasm.js" - ); + const { parse_java } = await loadParser("windmill-parser-wasm-java"); inferedSchema = JSON.parse(parse_java(content)); } else if (language === "ruby") { - const { parse_ruby } = await import( - "../../wasm/ruby/windmill_parser_wasm.js" - ); + const { parse_ruby } = await loadParser("windmill-parser-wasm-ruby"); inferedSchema = JSON.parse(parse_ruby(content)); // for related places search: ADD_NEW_LANG } else { @@ -751,16 +736,16 @@ export async function parseMetadataFile( ): Promise<{ isJson: boolean; payload: any; path: string }> { let metadataFilePath = scriptPath + ".script.json"; try { - await Deno.stat(metadataFilePath); + await stat(metadataFilePath); return { path: metadataFilePath, - payload: JSON.parse(await Deno.readTextFile(metadataFilePath)), + payload: JSON.parse(await readFile(metadataFilePath, "utf-8")), isJson: true, }; } catch { try { metadataFilePath = scriptPath + ".script.yaml"; - await Deno.stat(metadataFilePath); + await stat(metadataFilePath); const payload: any = await yamlParseFile(metadataFilePath); replaceLock(payload); @@ -785,12 +770,8 @@ export async function parseMetadataFile( yamlOptions ); - await Deno.writeTextFile(metadataFilePath, scriptInitialMetadataYaml, { - createNew: true, - }); - await Deno.writeTextFile(lockPath, "", { - createNew: true, - }); + await writeFile(metadataFilePath, scriptInitialMetadataYaml, { flag: "wx", encoding: "utf-8" }); + await writeFile(lockPath, "", { flag: "wx", encoding: "utf-8" }); if (generateMetadataIfMissing) { log.info( @@ -857,7 +838,7 @@ export async function readLockfile(): Promise { } } catch { const lock = { locks: {}, version: "v2" as const }; - await Deno.writeTextFile(WMILL_LOCKFILE, yamlStringify(lock, yamlOptions)); + await writeFile(WMILL_LOCKFILE, yamlStringify(lock, yamlOptions), "utf-8"); log.info(colors.green("wmill-lock.yaml created")); return lock; @@ -925,9 +906,10 @@ export async function clearGlobalLock(path: string): Promise { } }); } - await Deno.writeTextFile( + await writeFile( WMILL_LOCKFILE, - yamlStringify(conf as Record, yamlOptions) + yamlStringify(conf as Record, yamlOptions), + "utf-8" ); } } @@ -957,8 +939,9 @@ export async function updateMetadataGlobalLock( conf.locks[path] = hash; } } - await Deno.writeTextFile( + await writeFile( WMILL_LOCKFILE, - yamlStringify(conf as Record, yamlOptions) + yamlStringify(conf as Record, yamlOptions), + "utf-8" ); } diff --git a/cli/src/utils/resource_folders.ts b/cli/src/utils/resource_folders.ts index 5d7fc7c1ca..898edb305c 100644 --- a/cli/src/utils/resource_folders.ts +++ b/cli/src/utils/resource_folders.ts @@ -8,7 +8,9 @@ * (.flow, .app, .raw_app) or dunder-prefixed names (__flow, __app, __raw_app). */ -import { log, SEP, yamlParseFile } from "../../deps.ts"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; +import { yamlParseFile } from "./yaml.ts"; import * as fs from "node:fs"; import * as path from "node:path"; import process from "node:process"; @@ -154,25 +156,30 @@ export function getMetadataPathSuffix( // Path Detection Functions // ============================================================================ +/** Normalize path separators to forward slash for cross-platform matching */ +function normalizeSep(p: string): string { + return p.replaceAll("\\", "/"); +} + /** * Check if a path is inside a flow folder */ export function isFlowPath(p: string): boolean { - return p.includes(getFolderSuffixes().flow + SEP); + return normalizeSep(p).includes(getFolderSuffixes().flow + "/"); } /** * Check if a path is inside an app folder */ export function isAppPath(p: string): boolean { - return p.includes(getFolderSuffixes().app + SEP); + return normalizeSep(p).includes(getFolderSuffixes().app + "/"); } /** * Check if a path is inside a raw_app folder */ export function isRawAppPath(p: string): boolean { - return p.includes(getFolderSuffixes().raw_app + SEP); + return normalizeSep(p).includes(getFolderSuffixes().raw_app + "/"); } /** @@ -248,10 +255,11 @@ export function extractResourceName( p: string, type: FolderResourceType ): string | null { - const suffix = getFolderSuffixes()[type] + SEP; - const index = p.indexOf(suffix); + const normalized = normalizeSep(p); + const suffix = getFolderSuffixes()[type] + "/"; + const index = normalized.indexOf(suffix); if (index === -1) return null; - return p.substring(0, index); + return normalized.substring(0, index); } /** @@ -262,10 +270,11 @@ export function extractFolderPath( p: string, type: FolderResourceType ): string | null { - const suffix = getFolderSuffixes()[type] + SEP; - const index = p.indexOf(suffix); + const normalized = normalizeSep(p); + const suffix = getFolderSuffixes()[type] + "/"; + const index = normalized.indexOf(suffix); if (index === -1) return null; - return p.substring(0, index) + suffix; + return normalized.substring(0, index) + suffix; } /** @@ -291,7 +300,7 @@ export function buildMetadataPath( return ( resourceName + getFolderSuffixes()[type] + - SEP + + "/" + METADATA_FILES[type][format] ); } diff --git a/cli/src/utils/upgrade.ts b/cli/src/utils/upgrade.ts index 83bbe6b4ab..709d59eb7f 100644 --- a/cli/src/utils/upgrade.ts +++ b/cli/src/utils/upgrade.ts @@ -1,4 +1,4 @@ -import { Provider } from "../../deps.ts"; +import { Provider } from "@cliffy/command/upgrade"; export type NpmProviderOptions = { main?: string; logger?: any } & ( | { diff --git a/cli/src/utils/utils.ts b/cli/src/utils/utils.ts index b4fa85ba64..8448ef9b9a 100644 --- a/cli/src/utils/utils.ts +++ b/cli/src/utils/utils.ts @@ -2,8 +2,13 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck This file is copied from a JS project, so it's not type-safe. -import { colors, encodeHex, log, SEP } from "../../deps.ts"; +import { colors } from "@cliffy/ansi/colors"; +import { encodeHex } from "@std/encoding"; +import * as log from "@std/log"; +import { SEPARATOR as SEP } from "@std/path"; import crypto from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; +import { readdir, readFile } from "node:fs/promises"; import { fetchVersion } from "../core/context.ts"; import { updateGlobalVersions } from "../commands/sync/global.ts"; import { isRawAppPath } from "./resource_folders.ts"; @@ -86,7 +91,7 @@ export function deepEqual(a: T, b: T): boolean { } export function getHeaders(): Record | undefined { - const headers = Deno.env.get("HEADERS"); + const headers = process.env["HEADERS"]; if (headers) { const parsedHeaders = Object.fromEntries( headers.split(",").map((h) => h.split(":").map((s) => s.trim())) @@ -102,11 +107,12 @@ export function getHeaders(): Record | undefined { export async function digestDir(path: string, conf: string) { const hashes: string = []; - for await (const e of Deno.readDir(path)) { + const entries = await readdir(path, { withFileTypes: true }); + for (const e of entries) { const npath = path + "/" + e.name; - if (e.isFile) { - hashes.push(await generateHashFromBuffer(await Deno.readFile(npath))); - } else if (e.isDirectory && !e.isSymlink) { + if (e.isFile()) { + hashes.push(await generateHashFromBuffer(await readFile(npath))); + } else if (e.isDirectory() && !e.isSymbolicLink()) { hashes.push(await digestDir(npath, "")); } } @@ -125,13 +131,9 @@ export async function generateHashFromBuffer( return encodeHex(hashBuffer); } -// export async function readInlinePath(path: string): Promise { -// return await Deno.readTextFile(path.replaceAll("/", SEP)); -// } - export function readInlinePathSync(path: string): string { try { - return Deno.readTextFileSync(path.replaceAll("/", SEP)); + return readFileSync(path.replaceAll("/", SEP), "utf-8"); } catch (error) { log.warn(`Error reading inline path: ${path}, ${error}`); return ""; @@ -161,13 +163,10 @@ export function isWorkspaceDependencies(path: string): boolean { return path.startsWith("dependencies/"); } -export function printSync(input: string | Uint8Array, to = Deno.stdout) { - let bytesWritten = 0; - const bytes = - typeof input === "string" ? new TextEncoder().encode(input) : input; - while (bytesWritten < bytes.length) { - bytesWritten += to.writeSync(bytes.subarray(bytesWritten)); - } +export function printSync(input: string | Uint8Array) { + process.stdout.write( + typeof input === "string" ? input : Buffer.from(input) + ); } // Repository interface for shared selection logic @@ -194,7 +193,7 @@ export async function selectRepository( } // Check if we're in a non-interactive environment - const isInteractive = Deno.stdin.isTerminal() && Deno.stdout.isTerminal(); + const isInteractive = !!process.stdin.isTTY && !!process.stdout.isTTY; if (!isInteractive) { const repoPaths = repositories.map((r) => @@ -208,7 +207,7 @@ export async function selectRepository( } // Import Select dynamically to avoid dependency issues - const { Select } = await import("../../deps.ts"); + const { Select } = await import("@cliffy/prompt/select"); console.log( `\nMultiple repositories found. Please select which repository to ${ @@ -249,21 +248,19 @@ export async function getIsWin(): Promise { */ export function writeIfChanged(path: string, content: string): boolean { try { - const existing = Deno.readTextFileSync(path); + const existing = readFileSync(path, "utf-8"); if (existing === content) { - // console.log(`Content unchanged for ${path}`); return false; // Content unchanged, skip write } - } catch (error) { + } catch (error: any) { // File doesn't exist or can't be read, proceed with write - if (!(error instanceof Deno.errors.NotFound)) { + if (error?.code !== "ENOENT") { // If it's not a "not found" error, we might want to know about it // but still proceed with the write attempt } } - // console.log(`Writing content to ${path}`); - Deno.writeTextFileSync(path, content); + writeFileSync(path, content, "utf-8"); return true; // File was written } diff --git a/cli/src/utils/yaml.ts b/cli/src/utils/yaml.ts new file mode 100644 index 0000000000..f8621ba2b6 --- /dev/null +++ b/cli/src/utils/yaml.ts @@ -0,0 +1,22 @@ +import { parse as yamlParse, type ParseOptions } from "@std/yaml"; +import { readFile } from "node:fs/promises"; + +export async function yamlParseFile(path: string, options: ParseOptions = {}) { + try { + return yamlParse(await readFile(path, "utf-8"), options); + } catch (e) { + throw new Error(`Error parsing yaml ${path}`, { cause: e }); + } +} + +export function yamlParseContent( + path: string, + content: string, + options: ParseOptions = {}, +) { + try { + return yamlParse(content, options); + } catch (e) { + throw new Error(`Error parsing yaml ${path}`, { cause: e }); + } +} diff --git a/cli/test/cargo_backend.ts b/cli/test/cargo_backend.ts index 8e45c6bb12..7c85ff5bab 100644 --- a/cli/test/cargo_backend.ts +++ b/cli/test/cargo_backend.ts @@ -8,11 +8,16 @@ * - Backend code compiled or ready to compile * * Usage: - * DATABASE_URL=postgres://postgres:changeme@localhost:5432 deno test --allow-all test/my_test.ts + * DATABASE_URL=postgres://postgres:changeme@localhost:5432 bun test test/my_test.ts */ -import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts"; -import { fromFileUrl, resolve, dirname } from "https://deno.land/std@0.224.0/path/mod.ts"; +import { resolve, dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { statSync } from "node:fs"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { createServer } from "node:net"; +import { Subprocess } from "bun"; export interface CargoBackendConfig { /** PostgreSQL connection string (without database name) */ @@ -43,7 +48,7 @@ export interface CargoBackendConfig { export class CargoBackend { private config: Required; - private process: Deno.ChildProcess | null = null; + private process: Subprocess | null = null; private dbName: string; private isRunning = false; private actualPort: number; @@ -58,19 +63,21 @@ export class CargoBackend { // Determine default features based on environment // CI mode: minimal features (zip only) - // Local mode: full features (zip, private, enterprise) - const isCI = Deno.env.get("CI_MINIMAL_FEATURES") === "true"; - const defaultFeatures = isCI ? ["zip"] : ["zip", "private", "enterprise"]; + // Local mode with license key: full features (zip, private, enterprise, license) + // Local mode without license key: zip only (EE features reject API calls without valid license) + const isCI = process.env["CI_MINIMAL_FEATURES"] === "true"; + const hasLicenseKey = !!process.env["EE_LICENSE_KEY"]; + const defaultFeatures = isCI ? ["zip"] : (hasLicenseKey ? ["zip", "private", "enterprise", "license"] : ["zip"]); // Parse additional features from environment variable - const envFeatures = Deno.env.get("TEST_FEATURES")?.split(",").filter(f => f.trim()) || []; + const envFeatures = process.env["TEST_FEATURES"]?.split(",").filter(f => f.trim()) || []; const allFeatures = [...new Set([...defaultFeatures, ...envFeatures, ...(config.features || [])])]; this.config = { - postgresUrl: config.postgresUrl || Deno.env.get("DATABASE_URL") || "postgres://postgres:changeme@localhost:5432", + postgresUrl: config.postgresUrl || process.env["DATABASE_URL"] || "postgres://postgres:changeme@localhost:5432", port: config.port || 0, backendDir, - binaryPath: config.binaryPath || Deno.env.get("WINDMILL_BINARY") || "", + binaryPath: config.binaryPath || process.env["WINDMILL_BINARY"] || "", features: allFeatures, release: config.release ?? false, workspace: config.workspace || "test", @@ -84,8 +91,7 @@ export class CargoBackend { private findBackendDir(): string { // Try to find backend directory relative to CLI - // Use fromFileUrl to properly handle Windows paths (e.g., file:///D:/...) - const cliTestDir = fromFileUrl(new URL(".", import.meta.url)); + const cliTestDir = dirname(fileURLToPath(import.meta.url)); // Use resolve() for proper cross-platform path resolution const candidates = [ resolve(cliTestDir, "..", "..", "backend"), @@ -97,8 +103,8 @@ export class CargoBackend { for (const candidate of candidates) { try { const cargoPath = resolve(candidate, "Cargo.toml"); - const stat = Deno.statSync(cargoPath); - if (stat.isFile) { + const stat = statSync(cargoPath); + if (stat.isFile()) { return candidate; } } catch { @@ -129,19 +135,19 @@ export class CargoBackend { return; } - console.log("🚀 Starting Cargo-based Windmill backend..."); + console.log("Starting Cargo-based Windmill backend..."); // Create test config directory if (!this.config.testConfigDir) { - this.config.testConfigDir = await Deno.makeTempDir({ prefix: "wmill_test_config_" }); - console.log(`📁 Created test config directory: ${this.config.testConfigDir}`); + this.config.testConfigDir = await mkdtemp(join(tmpdir(), "wmill_test_config_")); + console.log(`Created test config directory: ${this.config.testConfigDir}`); } // Find a free port if not specified if (this.actualPort === 0) { this.actualPort = await this.findFreePort(); } - console.log(`📡 Using port: ${this.actualPort}`); + console.log(`Using port: ${this.actualPort}`); // Create the test database await this.createDatabase(); @@ -156,7 +162,7 @@ export class CargoBackend { await this.initializeAndAuthenticate(); this.isRunning = true; - console.log("✅ Cargo backend is ready!"); + console.log("Cargo backend is ready!"); console.log(` Server: ${this.baseUrl}`); console.log(` Database: ${this.dbName}`); console.log(` Workspace: ${this.config.workspace}`); @@ -170,15 +176,15 @@ export class CargoBackend { return; } - console.log("🛑 Stopping Cargo backend..."); + console.log("Stopping Cargo backend..."); // Kill the backend process if (this.process) { try { - this.process.kill("SIGTERM"); + this.process.kill(); // Wait a bit for graceful shutdown await Promise.race([ - this.process.status, + this.process.exited, new Promise(resolve => setTimeout(resolve, 5000)), ]); } catch { @@ -193,25 +199,29 @@ export class CargoBackend { // Cleanup test config directory if (this.config.testConfigDir?.includes("wmill_test_config_")) { try { - await Deno.remove(this.config.testConfigDir, { recursive: true }); - console.log(`🗑️ Cleaned up test config directory`); + await rm(this.config.testConfigDir, { recursive: true, force: true }); + console.log(`Cleaned up test config directory`); } catch { // Ignore cleanup errors } } this.isRunning = false; - console.log("✅ Backend stopped"); + console.log("Backend stopped"); } /** * Find a free port */ private async findFreePort(): Promise { - const listener = Deno.listen({ port: 0 }); - const port = (listener.addr as Deno.NetAddr).port; - listener.close(); - return port; + return new Promise((resolve, reject) => { + const server = createServer(); + server.listen(0, () => { + const port = (server.address() as any).port; + server.close(() => resolve(port)); + }); + server.on('error', reject); + }); } /** @@ -231,66 +241,65 @@ export class CargoBackend { * Create the test database */ private async createDatabase(): Promise { - console.log(`📦 Creating test database: ${this.dbName}`); + console.log(`Creating test database: ${this.dbName}`); const baseUrl = this.getBasePostgresUrl(); - const cmd = new Deno.Command("psql", { - args: [ - `${baseUrl}/postgres`, - "-c", - `CREATE DATABASE "${this.dbName}";`, - ], - stdout: "piped", - stderr: "piped", + const proc = Bun.spawn(["psql", `${baseUrl}/postgres`, "-c", `CREATE DATABASE "${this.dbName}";`], { + stdout: "pipe", + stderr: "pipe", }); - const result = await cmd.output(); - if (result.code !== 0) { - const stderr = new TextDecoder().decode(result.stderr); + const [stdout, stderr] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + const exitCode = await proc.exited; + + if (exitCode !== 0) { throw new Error(`Failed to create database: ${stderr}`); } - console.log("✅ Test database created"); + console.log("Test database created"); } /** * Drop the test database */ private async dropDatabase(): Promise { - console.log(`🗑️ Dropping test database: ${this.dbName}`); + console.log(`Dropping test database: ${this.dbName}`); const baseUrl = this.getBasePostgresUrl(); // Terminate existing connections - const terminateCmd = new Deno.Command("psql", { - args: [ - `${baseUrl}/postgres`, - "-c", - `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${this.dbName}' AND pid <> pg_backend_pid();`, - ], - stdout: "piped", - stderr: "piped", + const terminateProc = Bun.spawn(["psql", `${baseUrl}/postgres`, "-c", + `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${this.dbName}' AND pid <> pg_backend_pid();`], { + stdout: "pipe", + stderr: "pipe", }); - await terminateCmd.output(); + await Promise.all([ + new Response(terminateProc.stdout).text(), + new Response(terminateProc.stderr).text(), + ]); + await terminateProc.exited; // Drop the database - const dropCmd = new Deno.Command("psql", { - args: [ - `${baseUrl}/postgres`, - "-c", - `DROP DATABASE IF EXISTS "${this.dbName}";`, - ], - stdout: "piped", - stderr: "piped", + const dropProc = Bun.spawn(["psql", `${baseUrl}/postgres`, "-c", + `DROP DATABASE IF EXISTS "${this.dbName}";`], { + stdout: "pipe", + stderr: "pipe", }); - const result = await dropCmd.output(); - if (result.code !== 0) { - const stderr = new TextDecoder().decode(result.stderr); + const [, stderr] = await Promise.all([ + new Response(dropProc.stdout).text(), + new Response(dropProc.stderr).text(), + ]); + const exitCode = await dropProc.exited; + + if (exitCode !== 0) { console.warn(`Warning: Failed to drop database: ${stderr}`); } else { - console.log("✅ Test database dropped"); + console.log("Test database dropped"); } } @@ -305,7 +314,7 @@ export class CargoBackend { const databaseUrl = `${baseUrl}/${this.dbName}?sslmode=disable`; const env: Record = { - ...Deno.env.toObject(), + ...process.env as Record, DATABASE_URL: databaseUrl, PORT: String(this.actualPort), MODE: "standalone", // Run server + worker in one process @@ -324,24 +333,28 @@ export class CargoBackend { SUPERADMIN_PASSWORD: this.config.password, }; + // On Windows, ensure BUN_PATH and NODE_BIN_PATH are set for the worker. + // The Rust defaults (/usr/bin/bun, /usr/bin/node) don't exist on Windows. + if (process.platform === "win32") { + env.BUN_PATH = env.BUN_PATH || Bun.which("bun") || process.execPath; + env.NODE_BIN_PATH = env.NODE_BIN_PATH || Bun.which("node") || "node"; + } + // Add license key if available - const licenseKey = Deno.env.get("EE_LICENSE_KEY"); + const licenseKey = process.env["EE_LICENSE_KEY"]; if (licenseKey) { env.LICENSE_KEY = licenseKey; } - let cmd: Deno.Command; - if (this.config.binaryPath) { // Use pre-built binary if explicitly specified - console.log(`🔧 Starting backend using binary: ${this.config.binaryPath}`); + console.log(`Starting backend using binary: ${this.config.binaryPath}`); console.log(` DATABASE_URL: ${databaseUrl}`); - cmd = new Deno.Command(this.config.binaryPath, { - args: [], + this.process = Bun.spawn([this.config.binaryPath], { env, - stdout: "piped", - stderr: "piped", + stdout: "pipe", + stderr: "pipe", }); } else { // Use cargo run with features @@ -353,27 +366,25 @@ export class CargoBackend { cargoArgs.push("--features", this.config.features.join(",")); } - console.log(`🔧 Starting backend via: cargo ${cargoArgs.join(" ")}`); + console.log(`Starting backend via: cargo ${cargoArgs.join(" ")}`); console.log(` DATABASE_URL: ${databaseUrl}`); console.log(` Backend dir: ${this.config.backendDir}`); - cmd = new Deno.Command("cargo", { - args: cargoArgs, + this.process = Bun.spawn(["cargo", ...cargoArgs], { cwd: this.config.backendDir, env, - stdout: "piped", - stderr: "piped", + stdout: "pipe", + stderr: "pipe", }); } - this.process = cmd.spawn(); this.stderrChunks = []; this.stdoutChunks = []; // Capture output in background this.captureProcessOutput(); - console.log(`⏳ Backend process started (PID: ${this.process.pid})`); + console.log(`Backend process started (PID: ${this.process.pid})`); } /** @@ -395,7 +406,7 @@ export class CargoBackend { if (value) { this.stdoutChunks.push(value); if (this.config.verbose) { - Deno.stdout.writeSync(value); + process.stdout.write(value); } } } @@ -415,7 +426,7 @@ export class CargoBackend { if (value) { this.stderrChunks.push(value); if (this.config.verbose) { - Deno.stderr.writeSync(value); + process.stderr.write(value); } } } @@ -458,7 +469,7 @@ export class CargoBackend { * Wait for the API to be responsive */ private async waitForAPI(): Promise { - console.log("⏳ Waiting for API to be responsive (this may take a few minutes if compiling)..."); + console.log("Waiting for API to be responsive (this may take a few minutes if compiling)..."); // Allow up to 10 minutes for cargo build + startup const maxAttempts = 300; // 10 minutes with 2-second intervals @@ -473,7 +484,7 @@ export class CargoBackend { if (response.ok) { const version = await response.text(); - console.log(`📡 API ready (version: ${version.trim()})`); + console.log(`API ready (version: ${version.trim()})`); return; } await response.text(); // Consume response @@ -485,7 +496,7 @@ export class CargoBackend { if (this.process) { try { const status = await Promise.race([ - this.process.status, + this.process.exited, new Promise(resolve => setTimeout(() => resolve(null), 100)), ]); if (status !== null) { @@ -493,14 +504,14 @@ export class CargoBackend { await new Promise(resolve => setTimeout(resolve, 500)); const stderr = this.getStderr(); const stdout = this.getStdout(); - console.error("\n❌ Backend process crashed!"); + console.error("\nBackend process crashed!"); if (stdout) { console.error("=== STDOUT ===\n" + stdout.slice(-2000)); } if (stderr) { console.error("=== STDERR ===\n" + stderr.slice(-2000)); } - throw new Error(`Backend process exited with code ${status.code}`); + throw new Error(`Backend process exited with code ${status}`); } } catch (e) { if (e instanceof Error && e.message.includes("exited")) { @@ -529,7 +540,7 @@ export class CargoBackend { * Initialize test data and authenticate */ private async initializeAndAuthenticate(): Promise { - console.log("🔧 Initializing test workspace..."); + console.log("Initializing test workspace..."); // Create test workspace via API await this.createWorkspace(); @@ -537,7 +548,7 @@ export class CargoBackend { // Login to get token await this.authenticate(); - console.log("✅ Test workspace initialized"); + console.log("Test workspace initialized"); } /** @@ -581,7 +592,7 @@ export class CargoBackend { } } else { await createWsResponse.text(); - console.log(` ✅ Created workspace: ${this.config.workspace}`); + console.log(` Created workspace: ${this.config.workspace}`); } } @@ -589,7 +600,7 @@ export class CargoBackend { * Authenticate and get token */ private async authenticate(): Promise { - console.log("🔑 Authenticating..."); + console.log("Authenticating..."); const loginResponse = await fetch(`${this.baseUrl}/api/auth/login`, { method: "POST", @@ -605,7 +616,7 @@ export class CargoBackend { } this.token = await loginResponse.text(); - console.log("✅ Authentication successful"); + console.log("Authentication successful"); } /** @@ -618,8 +629,9 @@ export class CargoBackend { /** * Create CLI command with proper authentication */ - createCLICommand(args: string[], workingDir: string, workspaceName?: string): Deno.Command { + createCLICommand(args: string[], workingDir: string, workspaceName?: string): { command: string, args: string[], cwd: string, env: Record } { const workspace = workspaceName || this.config.workspace; + const cliDir = join(dirname(fileURLToPath(import.meta.url)), ".."); const fullArgs = [ "--base-url", this.baseUrl, "--workspace", workspace, @@ -628,20 +640,21 @@ export class CargoBackend { ...args, ]; - const denoPath = Deno.execPath(); - const cliMainPath = fromFileUrl(new URL("../src/main.ts", import.meta.url)); + const useNode = process.env["TEST_CLI_RUNTIME"] === "node"; + const runtime = useNode ? "node" : "bun"; + const entrypoint = useNode + ? join(cliDir, "npm", "esm", "main.js") + : join(cliDir, "src", "main.ts"); + const runtimeArgs = useNode ? [entrypoint] : ["run", entrypoint]; - console.log("🔧 CLI Command:", [denoPath, "run", "-A", cliMainPath, ...fullArgs].join(" ")); + console.log("CLI Command:", [runtime, ...runtimeArgs, ...fullArgs].join(" ")); - return new Deno.Command(denoPath, { - args: ["run", "-A", cliMainPath, ...fullArgs], + return { + command: runtime, + args: [...runtimeArgs, ...fullArgs], cwd: workingDir, - stdout: "piped", - stderr: "piped", - env: { - SKIP_DENO_DEPRECATION_WARNING: "true", - }, - }); + env: { ...process.env as Record }, + }; } /** @@ -653,13 +666,20 @@ export class CargoBackend { code: number; }> { const cmd = this.createCLICommand(args, workingDir, workspaceName); - const result = await cmd.output(); + const proc = Bun.spawn([cmd.command, ...cmd.args], { + cwd: cmd.cwd, + env: cmd.env, + stdout: "pipe", + stderr: "pipe", + }); - return { - stdout: new TextDecoder().decode(result.stdout), - stderr: new TextDecoder().decode(result.stderr), - code: result.code, - }; + const [stdout, stderr] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + const code = await proc.exited; + + return { stdout, stderr, code }; } /** @@ -677,7 +697,7 @@ export class CargoBackend { * Reset workspace to clean state */ async reset(): Promise { - console.log("🔄 Resetting workspace..."); + console.log("Resetting workspace..."); // Delete all content via API await Promise.all([ @@ -689,7 +709,7 @@ export class CargoBackend { this.deleteAll("folders"), ]); - console.log("✅ Workspace reset complete"); + console.log("Workspace reset complete"); } private async deleteAll(resourceType: string): Promise { @@ -731,13 +751,13 @@ export async function withCargoBackend( await globalCargoBackend.start(); } - const tempDir = await Deno.makeTempDir({ prefix: "windmill_cli_test_" }); + const tempDir = await mkdtemp(join(tmpdir(), "windmill_cli_test_")); try { await globalCargoBackend.reset(); return await testFn(globalCargoBackend, tempDir); } finally { - await Deno.remove(tempDir, { recursive: true }); + await rm(tempDir, { recursive: true, force: true }); } } @@ -752,15 +772,15 @@ export async function cleanupCargoBackend(): Promise { } /** - * Check if running in CI minimal mode (skip EE-dependent tests) + * Check if EE-dependent tests should be skipped * - * When CI_MINIMAL_FEATURES=true: - * - Backend runs with only "zip" feature (no private/enterprise) - * - Tests requiring EE features should be skipped + * Returns true when: + * - CI_MINIMAL_FEATURES=true (CI mode with zip-only features) + * - EE_LICENSE_KEY is not set (EE features reject API calls without valid license) * * Use this in test definitions: - * ignore: shouldSkipOnCI() + * test.skipIf(shouldSkipOnCI())("my EE test", ...) */ export function shouldSkipOnCI(): boolean { - return Deno.env.get("CI_MINIMAL_FEATURES") === "true"; + return process.env["CI_MINIMAL_FEATURES"] === "true" || !process.env["EE_LICENSE_KEY"]; } diff --git a/cli/test/cargo_backend_example.test.ts b/cli/test/cargo_backend_example.standalone.ts similarity index 55% rename from cli/test/cargo_backend_example.test.ts rename to cli/test/cargo_backend_example.standalone.ts index 49bc2c48b6..945b2b6419 100644 --- a/cli/test/cargo_backend_example.test.ts +++ b/cli/test/cargo_backend_example.standalone.ts @@ -16,76 +16,54 @@ * VERBOSE=1 deno test --allow-all test/cargo_backend_example.test.ts */ -import { - assertEquals, - assertExists, -} from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { CargoBackend } from "./cargo_backend.ts"; // Single backend instance for all tests let backend: CargoBackend; // Setup before all tests -Deno.test({ - name: "setup: start cargo backend", - fn: async () => { +test("setup: start cargo backend", async () => { backend = new CargoBackend({ - verbose: Deno.env.get("VERBOSE") === "1", + verbose: process.env.VERBOSE === "1", }); await backend.start(); - assertExists(backend.baseUrl); - assertExists(backend.authToken); - }, - sanitizeResources: false, - sanitizeOps: false, + expect(backend.baseUrl).toBeDefined(); + expect(backend.authToken).toBeDefined(); }); -Deno.test({ - name: "API: version endpoint responds", - fn: async () => { +test("API: version endpoint responds", async () => { const response = await fetch(`${backend.baseUrl}/api/version`); - assertEquals(response.ok, true); + expect(response.ok).toEqual(true); const version = await response.text(); - assertExists(version); + expect(version).toBeDefined(); console.log(` Backend version: ${version.trim()}`); - }, - sanitizeResources: false, - sanitizeOps: false, }); -Deno.test({ - name: "API: workspace exists", - fn: async () => { +test("API: workspace exists", async () => { const response = await backend.apiRequest( `/api/w/${backend.workspace}/workspaces/get_settings`, ); - assertEquals(response.ok, true); + expect(response.ok).toEqual(true); await response.text(); - }, - sanitizeResources: false, - sanitizeOps: false, }); -Deno.test({ - name: "CLI: wmill --version works", - fn: async () => { - const tempDir = await Deno.makeTempDir({ prefix: "wmill_test_" }); +test("CLI: wmill --version works", async () => { + const tempDir = await mkdtemp(join(tmpdir(), "wmill_test_")); try { const result = await backend.runCLICommand(["--version"], tempDir); - assertEquals(result.code, 0); + expect(result.code).toEqual(0); console.log(` CLI version: ${result.stdout.trim()}`); } finally { - await Deno.remove(tempDir, { recursive: true }); + await rm(tempDir, { recursive: true }); } - }, - sanitizeResources: false, - sanitizeOps: false, }); -Deno.test({ - name: "CLI: wmill sync pull works", - fn: async () => { - const tempDir = await Deno.makeTempDir({ prefix: "wmill_test_" }); +test("CLI: wmill sync pull works", async () => { + const tempDir = await mkdtemp(join(tmpdir(), "wmill_test_")); try { const result = await backend.runCLICommand( ["sync", "pull", "--yes"], @@ -97,19 +75,11 @@ Deno.test({ console.log(` stderr: ${result.stderr.slice(0, 200)}`); } } finally { - await Deno.remove(tempDir, { recursive: true }); + await rm(tempDir, { recursive: true }); } - }, - sanitizeResources: false, - sanitizeOps: false, }); // Cleanup after all tests -Deno.test({ - name: "cleanup: stop cargo backend", - fn: async () => { +test("cleanup: stop cargo backend", async () => { await backend.stop(); - }, - sanitizeResources: false, - sanitizeOps: false, }); diff --git a/cli/test/conf_branch_override.test.ts b/cli/test/conf_branch_override.test.ts index e0976710bb..1bacea0636 100644 --- a/cli/test/conf_branch_override.test.ts +++ b/cli/test/conf_branch_override.test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertExists } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; import { getEffectiveSettings, type SyncOptions } from "../src/core/conf.ts"; // ============================================================================= @@ -6,7 +6,7 @@ import { getEffectiveSettings, type SyncOptions } from "../src/core/conf.ts"; // Tests for getEffectiveSettings with branchOverride parameter // ============================================================================= -Deno.test("getEffectiveSettings: applies branch overrides when branchOverride is provided", async () => { +test("getEffectiveSettings: applies branch overrides when branchOverride is provided", async () => { const config: SyncOptions = { defaultTs: "bun", includes: ["f/**"], @@ -28,18 +28,18 @@ Deno.test("getEffectiveSettings: applies branch overrides when branchOverride is // Test with staging branch override const stagingSettings = await getEffectiveSettings(config, undefined, true, true, "staging"); - assertEquals(stagingSettings.includes, ["staging/**"]); - assertEquals(stagingSettings.skipVariables, true); - assertEquals(stagingSettings.skipSecrets, undefined); + expect(stagingSettings.includes).toEqual(["staging/**"]); + expect(stagingSettings.skipVariables).toEqual(true); + expect(stagingSettings.skipSecrets).toEqual(undefined); // Test with production branch override const prodSettings = await getEffectiveSettings(config, undefined, true, true, "production"); - assertEquals(prodSettings.includes, ["prod/**"]); - assertEquals(prodSettings.skipSecrets, true); - assertEquals(prodSettings.skipVariables, undefined); + expect(prodSettings.includes).toEqual(["prod/**"]); + expect(prodSettings.skipSecrets).toEqual(true); + expect(prodSettings.skipVariables).toEqual(undefined); }); -Deno.test("getEffectiveSettings: uses top-level settings when branchOverride has no overrides", async () => { +test("getEffectiveSettings: uses top-level settings when branchOverride has no overrides", async () => { const config: SyncOptions = { defaultTs: "bun", includes: ["f/**"], @@ -52,12 +52,12 @@ Deno.test("getEffectiveSettings: uses top-level settings when branchOverride has }; const settings = await getEffectiveSettings(config, undefined, true, true, "staging"); - assertEquals(settings.includes, ["f/**"]); - assertEquals(settings.skipVariables, true); - assertEquals(settings.defaultTs, "bun"); + expect(settings.includes).toEqual(["f/**"]); + expect(settings.skipVariables).toEqual(true); + expect(settings.defaultTs).toEqual("bun"); }); -Deno.test("getEffectiveSettings: uses top-level settings for unknown branch", async () => { +test("getEffectiveSettings: uses top-level settings for unknown branch", async () => { const config: SyncOptions = { defaultTs: "bun", includes: ["f/**"], @@ -71,11 +71,11 @@ Deno.test("getEffectiveSettings: uses top-level settings for unknown branch", as }; const settings = await getEffectiveSettings(config, undefined, true, true, "nonexistent"); - assertEquals(settings.includes, ["f/**"]); - assertEquals(settings.defaultTs, "bun"); + expect(settings.includes).toEqual(["f/**"]); + expect(settings.defaultTs).toEqual("bun"); }); -Deno.test("getEffectiveSettings: promotionOverrides take precedence when promotion specified", async () => { +test("getEffectiveSettings: promotionOverrides take precedence when promotion specified", async () => { const config: SyncOptions = { defaultTs: "bun", includes: ["f/**"], @@ -94,16 +94,16 @@ Deno.test("getEffectiveSettings: promotionOverrides take precedence when promoti // Test without promotion flag - should use regular overrides const normalSettings = await getEffectiveSettings(config, undefined, true, true, "production"); - assertEquals(normalSettings.includes, ["prod/**"]); - assertEquals(normalSettings.skipVariables, undefined); + expect(normalSettings.includes).toEqual(["prod/**"]); + expect(normalSettings.skipVariables).toEqual(undefined); // Test with promotion flag - should use promotionOverrides const promoSettings = await getEffectiveSettings(config, "production", true, true); - assertEquals(promoSettings.includes, ["promoted/**"]); - assertEquals(promoSettings.skipVariables, true); + expect(promoSettings.includes).toEqual(["promoted/**"]); + expect(promoSettings.skipVariables).toEqual(true); }); -Deno.test("getEffectiveSettings: branchOverride works without gitBranches config", async () => { +test("getEffectiveSettings: branchOverride works without gitBranches config", async () => { const config: SyncOptions = { defaultTs: "bun", includes: ["f/**"], @@ -111,11 +111,11 @@ Deno.test("getEffectiveSettings: branchOverride works without gitBranches config // Should not throw even with branchOverride but no gitBranches const settings = await getEffectiveSettings(config, undefined, true, true, "staging"); - assertEquals(settings.includes, ["f/**"]); - assertEquals(settings.defaultTs, "bun"); + expect(settings.includes).toEqual(["f/**"]); + expect(settings.defaultTs).toEqual("bun"); }); -Deno.test("getEffectiveSettings: preserves all top-level settings in merged result", async () => { +test("getEffectiveSettings: preserves all top-level settings in merged result", async () => { const config: SyncOptions = { defaultTs: "bun", includes: ["f/**"], @@ -134,11 +134,11 @@ Deno.test("getEffectiveSettings: preserves all top-level settings in merged resu }; const settings = await getEffectiveSettings(config, undefined, true, true, "staging"); - assertEquals(settings.defaultTs, "bun"); - assertEquals(settings.includes, ["f/**"]); - assertEquals(settings.excludes, ["*.test.ts"]); - assertEquals(settings.skipVariables, true); // Overridden - assertEquals(settings.skipResources, false); - assertEquals(settings.skipFlows, false); - assertEquals(settings.parallel, 4); + expect(settings.defaultTs).toEqual("bun"); + expect(settings.includes).toEqual(["f/**"]); + expect(settings.excludes).toEqual(["*.test.ts"]); + expect(settings.skipVariables).toEqual(true); // Overridden + expect(settings.skipResources).toEqual(false); + expect(settings.skipFlows).toEqual(false); + expect(settings.parallel).toEqual(4); }); diff --git a/cli/test/containerized_backend.ts b/cli/test/containerized_backend.ts index 8ee7e064f6..65c26c8373 100644 --- a/cli/test/containerized_backend.ts +++ b/cli/test/containerized_backend.ts @@ -3,6 +3,25 @@ * Manages real Windmill EE backend containers for CLI testing */ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +async function runCommand(cmd: string, args: string[], opts?: { cwd?: string, env?: Record }): Promise<{ code: number, stdout: string, stderr: string }> { + const proc = Bun.spawn([cmd, ...args], { + stdout: 'pipe', + stderr: 'pipe', + cwd: opts?.cwd, + env: { ...process.env, ...opts?.env }, + }); + const [code, stdout, stderr] = await Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + return { code, stdout, stderr }; +} + export interface ContainerConfig { composeFile?: string; baseUrl?: string; @@ -71,25 +90,19 @@ export class ContainerizedBackend { // Create isolated test config directory if not provided if (!this.config.testConfigDir) { - this.config.testConfigDir = await Deno.makeTempDir({ prefix: 'wmill_test_config_' }); + this.config.testConfigDir = await mkdtemp(join(tmpdir(), 'wmill_test_config_')); console.log(`📁 Created test config directory: ${this.config.testConfigDir}`); } // Start containers with EE license key - const startCmd = new Deno.Command('docker', { - args: ['compose', '-f', this.config.composeFile, 'up', '-d'], - stdout: 'piped', - stderr: 'piped', + const startResult = await runCommand('docker', ['compose', '-f', this.config.composeFile, 'up', '-d'], { env: { - ...Deno.env.toObject(), - ...(Deno.env.get('EE_LICENSE_KEY') && { EE_LICENSE_KEY: Deno.env.get('EE_LICENSE_KEY')! }) + ...(process.env.EE_LICENSE_KEY && { EE_LICENSE_KEY: process.env.EE_LICENSE_KEY }) } }); - const startResult = await startCmd.output(); if (startResult.code !== 0) { - const stderr = new TextDecoder().decode(startResult.stderr); - throw new Error(`Failed to start containers: ${stderr}`); + throw new Error(`Failed to start containers: ${startResult.stderr}`); } // Wait for services to be healthy @@ -117,22 +130,16 @@ export class ContainerizedBackend { console.log('🛑 Stopping containerized backend...'); - const stopCmd = new Deno.Command('docker', { - args: ['compose', '-f', this.config.composeFile, 'down', '-v'], - stdout: 'piped', - stderr: 'piped', + await runCommand('docker', ['compose', '-f', this.config.composeFile, 'down', '-v'], { env: { - ...Deno.env.toObject(), - ...(Deno.env.get('EE_LICENSE_KEY') && { EE_LICENSE_KEY: Deno.env.get('EE_LICENSE_KEY')! }) + ...(process.env.EE_LICENSE_KEY && { EE_LICENSE_KEY: process.env.EE_LICENSE_KEY }) } }); - - await stopCmd.output(); // Clean up test config directory if we created it if (this.config.testConfigDir && this.config.testConfigDir.includes('wmill_test_config_')) { try { - await Deno.remove(this.config.testConfigDir, { recursive: true }); + await rm(this.config.testConfigDir, { recursive: true }); console.log(`🗑️ Cleaned up test config directory: ${this.config.testConfigDir}`); } catch (error) { console.warn(`⚠️ Failed to clean up test config directory: ${error}`); @@ -1013,7 +1020,7 @@ export async function main( /** * Create CLI command with proper authentication */ - createCLICommand(args: string[], workingDir: string, workspaceName?: string): Deno.Command { + createCLICommand(args: string[], workingDir: string, workspaceName?: string): { cmd: string[], cwd: string } { const workspace = workspaceName || this.config.workspace; const fullArgs = [ '--base-url', this.config.baseUrl, @@ -1022,21 +1029,21 @@ export async function main( '--config-dir', this.config.testConfigDir, ...args ]; - - const denoPath = Deno.execPath(); - const cliMainPath = new URL('../src/main.ts', import.meta.url).pathname; - console.log('🔧 CLI Command:', [denoPath, 'run', '-A', cliMainPath, ...fullArgs].join(' ')); + const useNode = process.env["TEST_CLI_RUNTIME"] === "node"; + const cliDir = new URL('..', import.meta.url).pathname; + const entrypoint = useNode + ? new URL('../npm/esm/main.js', import.meta.url).pathname + : new URL('../src/main.ts', import.meta.url).pathname; + const runtime = useNode ? 'node' : 'bun'; + const runtimeArgs = useNode ? [entrypoint] : ['run', entrypoint]; - return new Deno.Command(denoPath, { - args: ['run', '-A', cliMainPath, ...fullArgs], + console.log('CLI Command:', [runtime, ...runtimeArgs, ...fullArgs].join(' ')); + + return { + cmd: [runtime, ...runtimeArgs, ...fullArgs], cwd: workingDir, - stdout: 'piped', - stderr: 'piped', - env: { - 'SKIP_DENO_DEPRECATION_WARNING': 'true' - } - }); + }; } /** @@ -1047,14 +1054,18 @@ export async function main( stderr: string; code: number; }> { - const cmd = this.createCLICommand(args, workingDir, workspaceName); - const result = await cmd.output(); - - return { - stdout: new TextDecoder().decode(result.stdout), - stderr: new TextDecoder().decode(result.stderr), - code: result.code - }; + const { cmd, cwd } = this.createCLICommand(args, workingDir, workspaceName); + const proc = Bun.spawn(cmd, { + stdout: 'pipe', + stderr: 'pipe', + cwd, + }); + const [code, stdout, stderr] = await Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + return { stdout, stderr, code }; } /** @@ -1192,42 +1203,30 @@ export async function main( ON CONFLICT (workspace_id, kind) DO UPDATE SET key = EXCLUDED.key; `; - const execCmd = new Deno.Command('docker', { - args: ['compose', '-f', this.config.composeFile, 'exec', '-T', 'test_db', - 'psql', '-U', 'postgres', '-d', 'windmill_test', '-c', initSQL], - stdout: 'piped', - stderr: 'piped', + const result = await runCommand('docker', ['compose', '-f', this.config.composeFile, 'exec', '-T', 'test_db', + 'psql', '-U', 'postgres', '-d', 'windmill_test', '-c', initSQL], { env: { - ...Deno.env.toObject(), - ...(Deno.env.get('EE_LICENSE_KEY') && { EE_LICENSE_KEY: Deno.env.get('EE_LICENSE_KEY')! }) + ...(process.env.EE_LICENSE_KEY && { EE_LICENSE_KEY: process.env.EE_LICENSE_KEY }) } }); - const result = await execCmd.output(); if (result.code !== 0) { - const stderr = new TextDecoder().decode(result.stderr); - throw new Error(`Failed to initialize test data: ${stderr}`); + throw new Error(`Failed to initialize test data: ${result.stderr}`); } console.log('✅ Test workspace initialized'); // Verify license key was stored - const checkLicenseCmd = new Deno.Command('docker', { - args: ['compose', '-f', this.config.composeFile, 'exec', '-T', 'test_db', - 'psql', '-U', 'postgres', '-d', 'windmill_test', '-c', - "SELECT name, value FROM global_settings WHERE name = 'license_key';"], - stdout: 'piped', - stderr: 'piped', + const checkResult = await runCommand('docker', ['compose', '-f', this.config.composeFile, 'exec', '-T', 'test_db', + 'psql', '-U', 'postgres', '-d', 'windmill_test', '-c', + "SELECT name, value FROM global_settings WHERE name = 'license_key';"], { env: { - ...Deno.env.toObject(), - EE_LICENSE_KEY: Deno.env.get('EE_LICENSE_KEY') || 'REMOVED_HARDCODED_LICENSE' + EE_LICENSE_KEY: process.env.EE_LICENSE_KEY || 'REMOVED_HARDCODED_LICENSE' } }); - - const checkResult = await checkLicenseCmd.output(); + if (checkResult.code === 0) { - const output = new TextDecoder().decode(checkResult.stdout); - console.log('🔍 License key in database:', output.trim()); + console.log('License key in database:', checkResult.stdout.trim()); } } @@ -1240,19 +1239,14 @@ export async function main( let attempts = 0; while (attempts < maxAttempts) { - const healthCmd = new Deno.Command('docker', { - args: ['compose', '-f', this.config.composeFile, 'ps', '--format', 'json'], - stdout: 'piped', - stderr: 'piped', + const result = await runCommand('docker', ['compose', '-f', this.config.composeFile, 'ps', '--format', 'json'], { env: { - ...Deno.env.toObject(), - ...(Deno.env.get('EE_LICENSE_KEY') && { EE_LICENSE_KEY: Deno.env.get('EE_LICENSE_KEY')! }) + ...(process.env.EE_LICENSE_KEY && { EE_LICENSE_KEY: process.env.EE_LICENSE_KEY }) } }); - - const result = await healthCmd.output(); + if (result.code === 0) { - const output = new TextDecoder().decode(result.stdout); + const output = result.stdout; if (output.trim()) { const containers = output.trim().split('\n').map(line => JSON.parse(line)); @@ -1345,15 +1339,15 @@ export async function withContainerizedBackend( } } - const tempDir = await Deno.makeTempDir({ prefix: 'windmill_cli_test_' }); - + const tempDir = await mkdtemp(join(tmpdir(), 'windmill_cli_test_')); + try { await globalBackend.reset(); await globalBackend.seedTestData(); - + return await testFn(globalBackend, tempDir); } finally { - await Deno.remove(tempDir, { recursive: true }); + await rm(tempDir, { recursive: true }); } } diff --git a/cli/test/dev_server.test.ts b/cli/test/dev_server.test.ts new file mode 100644 index 0000000000..be243b645d --- /dev/null +++ b/cli/test/dev_server.test.ts @@ -0,0 +1,417 @@ +/** + * Dev Server Smoke Tests + * + * Tests for `wmill dev` and `wmill app dev` commands. + * Verifies server startup, WebSocket connectivity, and file-change broadcasting. + * + * Run with: + * bun test test/dev_server.test.ts + */ + +import { expect, test } from "bun:test"; +import { writeFile, mkdir } from "node:fs/promises"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { createServer } from "node:net"; +import { Subprocess } from "bun"; +import WebSocket from "ws"; +import { withTestBackend } from "./test_backend.ts"; + +/** Find a free port by binding to port 0 */ +async function findFreePort(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + server.listen(0, () => { + const port = (server.address() as any).port; + server.close(() => resolve(port)); + }); + server.on("error", reject); + }); +} + +/** Wait for a condition with timeout */ +async function waitFor( + fn: () => T | Promise, + timeoutMs: number, + label: string, +): Promise { + const deadline = Date.now() + timeoutMs; + let lastError: unknown; + while (Date.now() < deadline) { + try { + const result = await fn(); + if (result) return result; + } catch (e) { + lastError = e; + } + await new Promise((r) => setTimeout(r, 200)); + } + throw new Error(`Timed out waiting for: ${label} (after ${timeoutMs}ms). Last error: ${lastError}`); +} + +/** Get CLI main.ts path */ +function getCLIMainPath(): string { + return join(dirname(fileURLToPath(import.meta.url)), "..", "src", "main.ts"); +} + +// ============================================================================= +// TEST 1: `wmill dev` smoke test +// ============================================================================= + +test( + "wmill dev: starts server, broadcasts file changes over WebSocket", + async () => { + await withTestBackend(async (backend, tempDir) => { + // Create wmill.yaml config + await writeFile( + join(tempDir, "wmill.yaml"), + "defaultTs: bun\n", + "utf-8", + ); + + // Create a script file + const scriptDir = join(tempDir, "f", "test"); + await mkdir(scriptDir, { recursive: true }); + await writeFile( + join(scriptDir, "hello.ts"), + 'export function main() { return "hello"; }\n', + "utf-8", + ); + await writeFile( + join(scriptDir, "hello.script.yaml"), + `summary: "test"\ndescription: ""\nlock: ""\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, + "utf-8", + ); + + // Push the script so the workspace has content + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes"], + tempDir, + ); + if (pushResult.code !== 0) { + console.error("Push stderr:", pushResult.stderr); + console.error("Push stdout:", pushResult.stdout); + } + expect(pushResult.code).toEqual(0); + + // Build the CLI command for `wmill dev` + const cliMainPath = getCLIMainPath(); + const args = [ + "run", + cliMainPath, + "--base-url", + backend.baseUrl, + "--workspace", + backend.workspace, + "--token", + backend.token!, + "--config-dir", + backend.testConfigDir, + "dev", + ]; + + let proc: Subprocess | null = null; + let ws: WebSocket | null = null; + + try { + // Spawn wmill dev as background process + proc = Bun.spawn(["bun", ...args], { + cwd: tempDir, + stdout: "pipe", + stderr: "pipe", + env: { ...process.env }, + }); + + // Read stdout to find the port + const stdoutReader = proc.stdout.getReader(); + let stdoutBuffer = ""; + let port: number | null = null; + + // Wait for "Server listening on port XXXX" message + const portMatch = await waitFor( + async () => { + try { + const { done, value } = await Promise.race([ + stdoutReader.read(), + new Promise<{ done: true; value: undefined }>((r) => + setTimeout(() => r({ done: true, value: undefined }), 500), + ), + ]); + if (!done && value) { + stdoutBuffer += new TextDecoder().decode(value); + } + } catch { + // Reader may be exhausted + } + const match = stdoutBuffer.match( + /Server listening on port (\d+)/, + ); + return match; + }, + 30000, + "dev server to start", + ); + + port = parseInt(portMatch[1], 10); + expect(port).toBeGreaterThan(0); + stdoutReader.releaseLock(); + + // Connect WebSocket + ws = new WebSocket(`ws://localhost:${port}`); + + // Wait for connection to open + await new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error("WebSocket connection timeout")), + 5000, + ); + ws!.on("open", () => { + clearTimeout(timeout); + resolve(); + }); + ws!.on("error", (err) => { + clearTimeout(timeout); + reject(err); + }); + }); + + expect(ws.readyState).toEqual(WebSocket.OPEN); + + // Set up a promise to receive the next WebSocket message + const isWindows = process.platform === "win32"; + const messagePromise = new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error("WebSocket message timeout")), + isWindows ? 30000 : 10000, + ); + ws!.on("message", (data) => { + clearTimeout(timeout); + try { + resolve(JSON.parse(data.toString())); + } catch (e) { + reject(e); + } + }); + }); + + // Modify the script file on disk + // Windows fs.watch() needs more time to initialize with recursive: true + await new Promise((r) => setTimeout(r, isWindows ? 2000 : 300)); + await writeFile( + join(scriptDir, "hello.ts"), + 'export function main() { return "modified"; }\n', + "utf-8", + ); + + // Wait for WebSocket message + const message = await messagePromise; + + // Verify the message + expect(message.type).toEqual("script"); + expect(message.content).toContain("modified"); + expect(message.path).toContain("f/test/hello"); + expect(message.language).toBeTruthy(); + } finally { + if (ws) { + ws.close(); + } + if (proc) { + proc.kill(); + await proc.exited; + } + } + }); + }, + { timeout: 60000 }, +); + +// ============================================================================= +// TEST 2: `wmill app dev` smoke test +// ============================================================================= + +test( + "wmill app dev: starts HTTP server, serves HTML, provides SSE endpoint", + async () => { + await withTestBackend(async (backend, tempDir) => { + // Create wmill.yaml config + await writeFile( + join(tempDir, "wmill.yaml"), + "defaultTs: bun\n", + "utf-8", + ); + + // Create a raw app directory with the right suffix + const appDir = join(tempDir, "f", "test", "myapp.raw_app"); + await mkdir(appDir, { recursive: true }); + + // Create raw_app.yaml + await writeFile( + join(appDir, "raw_app.yaml"), + `custom_path: f/test/myapp\n`, + "utf-8", + ); + + // Create package.json (minimal, with react dependency) + await writeFile( + join(appDir, "package.json"), + JSON.stringify( + { + name: "test-app", + private: true, + dependencies: { + react: "^18.0.0", + "react-dom": "^18.0.0", + }, + }, + null, + 2, + ), + "utf-8", + ); + + // Create index.tsx entry point + await writeFile( + join(appDir, "index.tsx"), + `import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; + +const root = createRoot(document.getElementById("root")!); +root.render(); +`, + "utf-8", + ); + + // Create App.tsx + await writeFile( + join(appDir, "App.tsx"), + `import React from "react"; + +export default function App() { + return
Hello from test app
; +} +`, + "utf-8", + ); + + // Run npm install in the app directory + const npmInstall = Bun.spawn(["npm", "install"], { + cwd: appDir, + stdout: "pipe", + stderr: "pipe", + }); + await Promise.all([ + new Response(npmInstall.stdout).text(), + new Response(npmInstall.stderr).text(), + ]); + const npmExitCode = await npmInstall.exited; + expect(npmExitCode).toEqual(0); + + // Find a free port + const port = await findFreePort(); + + // Build the CLI command for `wmill app dev` + const cliMainPath = getCLIMainPath(); + const args = [ + "run", + cliMainPath, + "--base-url", + backend.baseUrl, + "--workspace", + backend.workspace, + "--token", + backend.token!, + "--config-dir", + backend.testConfigDir, + "app", + "dev", + appDir, + "--no-open", + "--port", + String(port), + ]; + + let proc: Subprocess | null = null; + + try { + // Spawn wmill app dev as background process + proc = Bun.spawn(["bun", ...args], { + cwd: tempDir, + stdout: "pipe", + stderr: "pipe", + env: { ...process.env }, + }); + + // Collect stderr in background for debugging + const stderrReader = proc.stderr.getReader(); + let stderrBuffer = ""; + (async () => { + try { + while (true) { + const { done, value } = await stderrReader.read(); + if (done) break; + stderrBuffer += new TextDecoder().decode(value); + } + } catch { + // Process may have exited + } + })(); + + // Wait for server to be ready by polling the HTTP endpoint + await waitFor( + async () => { + try { + const res = await fetch(`http://localhost:${port}/`, { + signal: AbortSignal.timeout(1000), + }); + if (res.ok) { + await res.text(); + return true; + } + await res.text(); + } catch { + // Not ready yet + } + return false; + }, + 60000, + "app dev server to be ready", + ); + + // Verify GET / returns HTML + const htmlRes = await fetch(`http://localhost:${port}/`); + const contentType = htmlRes.headers.get("content-type"); + const htmlBody = await htmlRes.text(); + expect(contentType).toContain("text/html"); + expect(htmlBody).toContain(""); + expect(htmlBody).toContain("
"); + + // Verify GET /__events returns SSE stream + const controller = new AbortController(); + const sseTimeout = setTimeout(() => controller.abort(), 5000); + try { + const sseRes = await fetch(`http://localhost:${port}/__events`, { + signal: controller.signal, + }); + const sseContentType = sseRes.headers.get("content-type"); + expect(sseContentType).toContain("text/event-stream"); + // Read a small chunk to verify SSE sends data + const reader = sseRes.body!.getReader(); + const { value } = await reader.read(); + const chunk = new TextDecoder().decode(value); + expect(chunk).toContain("data: connected"); + reader.cancel(); + } finally { + clearTimeout(sseTimeout); + } + } finally { + if (proc) { + proc.kill(); + await proc.exited; + } + } + }); + }, + { timeout: 120000 }, +); + diff --git a/cli/test/elements_to_map_branch_specific.test.ts b/cli/test/elements_to_map_branch_specific.test.ts index 03af7d52ba..b9abaa8dd1 100644 --- a/cli/test/elements_to_map_branch_specific.test.ts +++ b/cli/test/elements_to_map_branch_specific.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; // Import the function we need to test import { elementsToMap } from "../src/commands/sync/sync.ts"; @@ -63,7 +63,7 @@ const defaultSkips = {}; // REGRESSION TEST: Remote base files should NOT be skipped // ============================================================================= -Deno.test("elementsToMap: remote base file is NOT skipped when configured as branch-specific (isRemote=true)", async () => { +test("elementsToMap: remote base file is NOT skipped when configured as branch-specific (isRemote=true)", async () => { // This is the key regression test. // When pulling from remote, the workspace only has base paths (e.g., TestVar.variable.yaml) // These should NOT be skipped even if configured as branch-specific, because the remote @@ -94,14 +94,10 @@ Deno.test("elementsToMap: remote base file is NOT skipped when configured as bra ); // The base file should be in the map - assertEquals( - Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml"), - true, - "Remote base file should NOT be skipped when isRemote=true" - ); + expect(Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true); }); -Deno.test("elementsToMap: local base file IS skipped when configured as branch-specific (isRemote=false)", async () => { +test("elementsToMap: local base file IS skipped when configured as branch-specific (isRemote=false)", async () => { // When processing local files, if a base file is configured as branch-specific, // it should be skipped because we expect the branch-specific version to be used instead. @@ -130,14 +126,10 @@ Deno.test("elementsToMap: local base file IS skipped when configured as branch-s ); // The base file should NOT be in the map (skipped because branch-specific expected) - assertEquals( - Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml"), - false, - "Local base file SHOULD be skipped when isRemote=false and configured as branch-specific" - ); + expect(Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(false); }); -Deno.test("elementsToMap: local branch-specific file is mapped to base path (isRemote=false)", async () => { +test("elementsToMap: local branch-specific file is mapped to base path (isRemote=false)", async () => { // When processing local files with branch-specific naming, they should be mapped to base paths const config: SpecificItemsConfig = { @@ -164,15 +156,8 @@ Deno.test("elementsToMap: local branch-specific file is mapped to base path (isR ); // The branch-specific file should be mapped to the base path - assertEquals( - Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml"), - true, - "Branch-specific file should be mapped to base path" - ); - assertEquals( - result["f/Shared/Variable/TestVar.variable.yaml"], - "value: staging-test\nis_secret: false", - ); + expect(Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true); + expect(result["f/Shared/Variable/TestVar.variable.yaml"]).toEqual("value: staging-test\nis_secret: false"); }); // ============================================================================= @@ -183,7 +168,7 @@ Deno.test("elementsToMap: local branch-specific file is mapped to base path (isR // - Expected: No deletion, the files should match // ============================================================================= -Deno.test("elementsToMap: pull scenario - remote and local maps should align correctly", async () => { +test("elementsToMap: pull scenario - remote and local maps should align correctly", async () => { const config: SpecificItemsConfig = { variables: ["f/Shared/Variable/**"], }; @@ -233,23 +218,15 @@ Deno.test("elementsToMap: pull scenario - remote and local maps should align cor const remoteKeys = Object.keys(remoteMap); const localKeys = Object.keys(localMap); - assertEquals( - remoteKeys.includes("f/Shared/Variable/TestVar.variable.yaml"), - true, - "Remote map should include base path" - ); - assertEquals( - localKeys.includes("f/Shared/Variable/TestVar.variable.yaml"), - true, - "Local map should include base path (mapped from branch-specific)" - ); + expect(remoteKeys.includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true); + expect(localKeys.includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true); }); // ============================================================================= // NON-CONFIGURED ITEMS: Should work the same regardless of isRemote // ============================================================================= -Deno.test("elementsToMap: non-configured items included regardless of isRemote", async () => { +test("elementsToMap: non-configured items included regardless of isRemote", async () => { const config: SpecificItemsConfig = { variables: ["f/Other/**"], // Only "Other" folder is branch-specific }; @@ -286,23 +263,15 @@ Deno.test("elementsToMap: non-configured items included regardless of isRemote", ); // Both should include the file since it's not in the branch-specific config - assertEquals( - Object.keys(remoteResult).includes("f/Shared/Variable/TestVar.variable.yaml"), - true, - "Non-configured item should be included when isRemote=true" - ); - assertEquals( - Object.keys(localResult).includes("f/Shared/Variable/TestVar.variable.yaml"), - true, - "Non-configured item should be included when isRemote=false" - ); + expect(Object.keys(remoteResult).includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true); + expect(Object.keys(localResult).includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true); }); // ============================================================================= // RESOURCE TYPE TESTS // ============================================================================= -Deno.test("elementsToMap: remote resource base file not skipped when configured", async () => { +test("elementsToMap: remote resource base file not skipped when configured", async () => { const config: SpecificItemsConfig = { resources: ["f/db/**"], }; @@ -326,18 +295,14 @@ Deno.test("elementsToMap: remote resource base file not skipped when configured" true, // isRemote ); - assertEquals( - Object.keys(result).includes("f/db/connection.resource.yaml"), - true, - "Remote resource base file should NOT be skipped" - ); + expect(Object.keys(result).includes("f/db/connection.resource.yaml")).toEqual(true); }); // ============================================================================= // TRIGGER TYPE TESTS // ============================================================================= -Deno.test("elementsToMap: remote trigger base file not skipped when configured", async () => { +test("elementsToMap: remote trigger base file not skipped when configured", async () => { const config: SpecificItemsConfig = { triggers: ["f/webhooks/**"], }; @@ -361,18 +326,14 @@ Deno.test("elementsToMap: remote trigger base file not skipped when configured", true, // isRemote ); - assertEquals( - Object.keys(result).includes("f/webhooks/handler.http_trigger.yaml"), - true, - "Remote trigger base file should NOT be skipped" - ); + expect(Object.keys(result).includes("f/webhooks/handler.http_trigger.yaml")).toEqual(true); }); // ============================================================================= // SETTINGS TYPE TESTS // ============================================================================= -Deno.test("elementsToMap: remote settings.yaml not skipped when configured", async () => { +test("elementsToMap: remote settings.yaml not skipped when configured", async () => { const config: SpecificItemsConfig = { settings: true, }; @@ -396,18 +357,14 @@ Deno.test("elementsToMap: remote settings.yaml not skipped when configured", asy true, // isRemote ); - assertEquals( - Object.keys(result).includes("settings.yaml"), - true, - "Remote settings.yaml should NOT be skipped" - ); + expect(Object.keys(result).includes("settings.yaml")).toEqual(true); }); // ============================================================================= // FOLDER TYPE TESTS // ============================================================================= -Deno.test("elementsToMap: remote folder meta not skipped when configured", async () => { +test("elementsToMap: remote folder meta not skipped when configured", async () => { const config: SpecificItemsConfig = { folders: ["f/env_*"], }; @@ -431,18 +388,14 @@ Deno.test("elementsToMap: remote folder meta not skipped when configured", async true, // isRemote ); - assertEquals( - Object.keys(result).includes("f/env_staging/folder.meta.yaml"), - true, - "Remote folder meta should NOT be skipped" - ); + expect(Object.keys(result).includes("f/env_staging/folder.meta.yaml")).toEqual(true); }); // ============================================================================= // BACKWARD COMPATIBILITY: isRemote undefined behaves like local (false) // ============================================================================= -Deno.test("elementsToMap: isRemote undefined behaves like local (backward compatible)", async () => { +test("elementsToMap: isRemote undefined behaves like local (backward compatible)", async () => { const config: SpecificItemsConfig = { variables: ["f/**"], }; @@ -468,9 +421,5 @@ Deno.test("elementsToMap: isRemote undefined behaves like local (backward compat ); // Base file should be skipped (same behavior as isRemote=false) - assertEquals( - Object.keys(result).includes("f/test.variable.yaml"), - false, - "isRemote undefined should behave like isRemote=false (skip base file)" - ); + expect(Object.keys(result).includes("f/test.variable.yaml")).toEqual(false); }); diff --git a/cli/test/folder_schedule_push.test.ts b/cli/test/folder_schedule_push.test.ts new file mode 100644 index 0000000000..c84e5b0ca8 --- /dev/null +++ b/cli/test/folder_schedule_push.test.ts @@ -0,0 +1,409 @@ +/** + * Integration tests for folder and schedule CLI commands. + * Tests list and push operations via CLI and direct API. + */ + +import { expect, test, describe } from "bun:test"; +import { writeFile, mkdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { withTestBackend } from "./test_backend.ts"; +import { addWorkspace } from "../workspace.ts"; + +async function setupWorkspaceProfile(backend: any): Promise { + await addWorkspace( + { + remote: backend.baseUrl, + workspaceId: backend.workspace, + name: "localhost_test", + token: backend.token, + }, + { force: true, configDir: backend.testConfigDir } + ); +} + +// ============================================================================= +// Folder Tests +// ============================================================================= + +describe("folder", () => { + test("list returns seeded folders", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand(["folder"], tempDir); + + expect(result.code).toEqual(0); + // seedTestData creates "test" folder + expect(result.stdout).toContain("test"); + }); + }); + + test("push creates a new folder via sync push", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const folderName = `inttest${uniqueId}`; + + // Create wmill.yaml + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + + // Create folder meta file + await mkdir(join(tempDir, "f", folderName), { recursive: true }); + await writeFile( + join(tempDir, "f", folderName, "folder.meta.yaml"), + `display_name: "Integration Test Folder ${uniqueId}"\nowners:\n - "admin@windmill.dev"\nextra_perms: {}\n`, + "utf-8" + ); + + // Push + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--includes", `f/${folderName}/**`], + tempDir + ); + + expect(pushResult.code).toEqual(0); + + // Verify folder was created via API + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/folders/get/${folderName}` + ); + expect(apiResp.status).toEqual(200); + const folderData = await apiResp.json(); + expect(folderData.name).toBe(folderName); + }); + }); + + test("push updates an existing folder", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const folderName = `updfolder${uniqueId}`; + + // Create folder via API + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/folders/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: folderName }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + // Create wmill.yaml and updated folder meta + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + await mkdir(join(tempDir, "f", folderName), { recursive: true }); + await writeFile( + join(tempDir, "f", folderName, "folder.meta.yaml"), + `display_name: "Updated Display Name"\nowners:\n - "u/admin"\nextra_perms:\n u/admin: true\n`, + "utf-8" + ); + + // Push the update + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--includes", `f/${folderName}/**`], + tempDir + ); + + expect(pushResult.code).toEqual(0); + + // Verify the display_name was updated + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/folders/get/${folderName}` + ); + expect(apiResp.status).toEqual(200); + const folderData = await apiResp.json(); + expect(folderData.display_name).toBe("Updated Display Name"); + }); + }); + + test("pull retrieves folder metadata", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const folderName = `pullfolder${uniqueId}`; + + // Create folder via API + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/folders/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: folderName }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + // Create wmill.yaml + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "f/${folderName}/**"\nexcludes: []\nskipVariables: true\nskipResources: true\n`, + "utf-8" + ); + + // Pull + const pullResult = await backend.runCLICommand( + ["sync", "pull", "--yes"], + tempDir + ); + expect(pullResult.code).toEqual(0); + + // Check the folder meta file was created + const content = await readFile( + join(tempDir, "f", folderName, "folder.meta.yaml"), "utf-8" + ); + expect(content).toBeDefined(); + }); + }); +}); + +// ============================================================================= +// Schedule Tests +// ============================================================================= + +describe("schedule", () => { + test("list returns empty table for fresh workspace", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand(["schedule"], tempDir); + + expect(result.code).toEqual(0); + // Table headers should be present + expect(result.stdout).toContain("Path"); + expect(result.stdout).toContain("Schedule"); + }); + }); + + test("push creates a schedule targeting an existing script", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + + // First create a script that the schedule can target + const scriptResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: `f/test/sched_target_${uniqueId}`, + content: 'export async function main() { return "ok"; }', + language: "bun", + summary: "Schedule target script", + description: "", + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + required: [], + }, + }), + } + ); + expect(scriptResp.status).toBeLessThan(300); + await scriptResp.text(); + + // Create wmill.yaml with includeSchedules + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\nincludeSchedules: true\n`, + "utf-8" + ); + + // Create schedule file + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + await writeFile( + join(tempDir, `f/test/cron_${uniqueId}.schedule.yaml`), + `path: "f/test/cron_${uniqueId}"\nschedule: "0 0 */6 * * *"\nscript_path: "f/test/sched_target_${uniqueId}"\nis_flow: false\nargs: {}\nenabled: false\ntimezone: "UTC"\n`, + "utf-8" + ); + + // Push + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--includes", `f/test/cron_${uniqueId}**`], + tempDir + ); + + expect(pushResult.code).toEqual(0); + + // Verify via API + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/schedules/get/f/test/cron_${uniqueId}` + ); + expect(apiResp.status).toEqual(200); + const schedData = await apiResp.json(); + expect(schedData.schedule).toBe("0 0 */6 * * *"); + expect(schedData.script_path).toBe(`f/test/sched_target_${uniqueId}`); + expect(schedData.enabled).toBe(false); + }); + }); + + test("push updates a schedule's cron expression", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + + // Create target script via API + const scriptResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: `f/test/upd_sched_target_${uniqueId}`, + content: 'export async function main() { return "ok"; }', + language: "bun", + summary: "Target", + description: "", + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + required: [], + }, + }), + } + ); + expect(scriptResp.status).toBeLessThan(300); + await scriptResp.text(); + + // Create schedule via API + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/schedules/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: `f/test/upd_cron_${uniqueId}`, + schedule: "0 0 * * * *", + script_path: `f/test/upd_sched_target_${uniqueId}`, + is_flow: false, + args: {}, + enabled: false, + timezone: "UTC", + }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + // Create wmill.yaml with includeSchedules and updated schedule + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\nincludeSchedules: true\n`, + "utf-8" + ); + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + await writeFile( + join(tempDir, `f/test/upd_cron_${uniqueId}.schedule.yaml`), + `path: "f/test/upd_cron_${uniqueId}"\nschedule: "0 30 2 * * *"\nscript_path: "f/test/upd_sched_target_${uniqueId}"\nis_flow: false\nargs: {}\nenabled: false\ntimezone: "UTC"\n`, + "utf-8" + ); + + // Push the update + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--includes", `f/test/upd_cron_${uniqueId}**`], + tempDir + ); + + expect(pushResult.code).toEqual(0); + + // Verify the schedule was updated + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/schedules/get/f/test/upd_cron_${uniqueId}` + ); + expect(apiResp.status).toEqual(200); + const schedData = await apiResp.json(); + expect(schedData.schedule).toBe("0 30 2 * * *"); + }); + }); + + test("pull retrieves schedules into local files", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + + // Create target script via API + const scriptResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: `f/test/pull_sched_target_${uniqueId}`, + content: 'export async function main() { return "ok"; }', + language: "bun", + summary: "Target for pull test", + description: "", + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + required: [], + }, + }), + } + ); + expect(scriptResp.status).toBeLessThan(300); + await scriptResp.text(); + + // Create schedule via API + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/schedules/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: `f/test/pull_cron_${uniqueId}`, + schedule: "0 15 3 * * 1", + script_path: `f/test/pull_sched_target_${uniqueId}`, + is_flow: false, + args: {}, + enabled: false, + timezone: "UTC", + }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + // Create wmill.yaml + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "f/test/pull_cron_${uniqueId}**"\nexcludes: []\nincludeSchedules: true\nskipVariables: true\nskipResources: true\nskipScripts: true\n`, + "utf-8" + ); + + // Pull + const pullResult = await backend.runCLICommand( + ["sync", "pull", "--yes"], + tempDir + ); + expect(pullResult.code).toEqual(0); + + // Check the schedule file was created + const content = await readFile( + join(tempDir, `f/test/pull_cron_${uniqueId}.schedule.yaml`), "utf-8" + ); + expect(content).toContain("0 15 3 * * 1"); + expect(content).toContain(`f/test/pull_sched_target_${uniqueId}`); + }); + }); +}); diff --git a/cli/test/generate_metadata.test.ts b/cli/test/generate_metadata.test.ts new file mode 100644 index 0000000000..f8b1cbf997 --- /dev/null +++ b/cli/test/generate_metadata.test.ts @@ -0,0 +1,230 @@ +/** + * Tests for WASM schema parsing across all supported languages. + * + * Calls `inferSchema` directly — no backend needed, fully local. + * Verifies that each language's WASM parser loads correctly and produces + * the expected JSON schema output. + */ + +import { expect, test, describe } from "bun:test"; +import { inferSchema } from "../src/utils/metadata.ts"; +import type { ScriptLanguage } from "../src/utils/script_common.ts"; + +interface LanguageTestCase { + language: ScriptLanguage; + content: string; + /** Property name to verify in schema.properties */ + expectedParam: string; + /** Expected JSON schema type, or undefined to skip type check */ + expectedType?: string; + /** If set, verify this resource format exists on the named param */ + expectedResourceParam?: { name: string; format: string }; +} + +const languageTestCases: LanguageTestCase[] = [ + { + language: "python3", + content: `def main(x: str):\n return x\n`, + expectedParam: "x", + expectedType: "string", + }, + { + language: "bun", + content: `export async function main(x: string) {\n return x;\n}\n`, + expectedParam: "x", + expectedType: "string", + }, + { + language: "deno", + content: `export async function main(x: string) {\n return x;\n}\n`, + expectedParam: "x", + expectedType: "string", + }, + { + language: "nativets", + content: `export async function main(x: string) {\n return x;\n}\n`, + expectedParam: "x", + expectedType: "string", + }, + { + language: "go", + content: `package inner\n\nfunc main(x string) (interface{}, error) {\n\treturn x, nil\n}\n`, + expectedParam: "x", + expectedType: "string", + }, + { + language: "bash", + // Bash parser infers params from variable assignments like x="$1" + content: `x="$1"\necho "$x"\n`, + expectedParam: "x", + expectedType: "string", + }, + { + language: "powershell", + content: `param([string]$x)\nWrite-Output $x\n`, + expectedParam: "x", + expectedType: "string", + }, + { + language: "postgresql", + content: `-- $1 name = default :: text\nSELECT $1::TEXT\n`, + expectedParam: "name", + expectedType: "string", + expectedResourceParam: { name: "database", format: "resource-postgresql" }, + }, + { + language: "mysql", + // MySQL parser only auto-detects the database resource param + content: `SELECT 1\n`, + expectedParam: "database", + expectedType: "object", + expectedResourceParam: { name: "database", format: "resource-mysql" }, + }, + { + language: "bigquery", + content: `SELECT 1\n`, + expectedParam: "database", + expectedType: "object", + expectedResourceParam: { name: "database", format: "resource-bigquery" }, + }, + { + language: "snowflake", + content: `SELECT 1\n`, + expectedParam: "database", + expectedType: "object", + expectedResourceParam: { name: "database", format: "resource-snowflake" }, + }, + { + language: "mssql", + content: `SELECT 1\n`, + expectedParam: "database", + expectedType: "object", + expectedResourceParam: { + name: "database", + format: "resource-ms_sql_server", + }, + }, + { + language: "oracledb", + content: `SELECT 1 FROM dual\n`, + expectedParam: "database", + expectedType: "object", + expectedResourceParam: { name: "database", format: "resource-oracledb" }, + }, + { + language: "duckdb", + // DuckDB parser doesn't auto-add a database resource + content: `SELECT 1\n`, + expectedParam: undefined as any, + expectedType: undefined, + }, + { + language: "graphql", + content: `query($name: String) {\n user(name: $name) { id }\n}\n`, + expectedParam: "name", + expectedType: "string", + expectedResourceParam: { name: "api", format: "resource-graphql" }, + }, + { + language: "php", + content: ` Result {\n Ok(x)\n}\n`, + expectedParam: "x", + expectedType: "string", + }, + { + language: "csharp", + content: `class Script {\n public static string Main(string x) {\n return x;\n }\n}\n`, + expectedParam: "x", + expectedType: "string", + }, + { + language: "nu", + content: `def main [x: string] {\n print $x\n}\n`, + expectedParam: "x", + expectedType: "string", + }, + { + language: "ansible", + content: `---\ninventory:\n - resource_type: ansible_inventory\n---\n- name: Test\n hosts: 127.0.0.1\n connection: local\n tasks:\n - name: Echo\n debug:\n msg: "hello"\n`, + // Ansible parser produces "inventory.ini" as param name + expectedParam: "inventory.ini", + expectedType: undefined, + }, + { + language: "java", + content: `public class Main {\n public static String main(String x) {\n return x;\n }\n}\n`, + expectedParam: "x", + expectedType: "string", + }, + { + language: "ruby", + content: `def main(x)\n puts x\nend\n`, + expectedParam: "x", + expectedType: undefined, // Ruby is dynamically typed + }, +]; + +describe("generate-metadata schema parsing", () => { + for (const tc of languageTestCases) { + test(`${tc.language}: WASM parser loads and infers schema`, async () => { + const result = await inferSchema( + tc.language, + tc.content, + {}, + `test.${tc.language}` + ); + + expect(result).toBeDefined(); + expect(result.schema).toBeDefined(); + expect(result.schema.properties).toBeDefined(); + + if (tc.expectedParam) { + expect(result.schema.properties[tc.expectedParam]).toBeDefined(); + + if (tc.expectedType !== undefined) { + expect(result.schema.properties[tc.expectedParam].type).toEqual( + tc.expectedType + ); + } + } + + if (tc.expectedResourceParam) { + const rp = result.schema.properties[tc.expectedResourceParam.name]; + expect(rp).toBeDefined(); + expect(rp.type).toEqual("object"); + expect(rp.format).toEqual(tc.expectedResourceParam.format); + } + }); + } +}); + +const allLanguages: ScriptLanguage[] = [ + "python3", "bun", "deno", "nativets", "go", "bash", "powershell", + "postgresql", "mysql", "bigquery", "snowflake", "mssql", "oracledb", + "duckdb", "graphql", "php", "rust", "csharp", "nu", "ansible", "java", "ruby", +]; + +describe("generate-metadata invalid input handling", () => { + for (const lang of allLanguages) { + test(`${lang}: does not crash on invalid input`, async () => { + const result = await inferSchema( + lang, + "THIS IS INVALID GARBAGE @#$%^&*()", + {}, + `test.${lang}` + ); + + expect(result).toBeDefined(); + expect(result.schema).toBeDefined(); + expect(result.schema.properties).toBeDefined(); + // Should return a valid (possibly empty) schema, not throw + expect(typeof result.schema.properties).toBe("object"); + }); + } +}); diff --git a/cli/test/git_unit.test.ts b/cli/test/git_unit.test.ts new file mode 100644 index 0000000000..bcbd0312fd --- /dev/null +++ b/cli/test/git_unit.test.ts @@ -0,0 +1,73 @@ +/** + * Unit tests for git utility functions. + * Tests pure functions only — no git subprocess calls. + */ + +import { expect, test, describe } from "bun:test"; +import { + getOriginalBranchForWorkspaceForks, + getWorkspaceIdForWorkspaceForkFromBranchName, +} from "../src/utils/git.ts"; + +// ============================================================================= +// getOriginalBranchForWorkspaceForks +// ============================================================================= + +describe("getOriginalBranchForWorkspaceForks", () => { + test("extracts original branch from valid fork branch name", () => { + expect(getOriginalBranchForWorkspaceForks("wm-fork/main/my-workspace")).toBe("main"); + }); + + test("extracts multi-segment original branch", () => { + expect( + getOriginalBranchForWorkspaceForks("wm-fork/feature/cool-thing/my-workspace") + ).toBe("feature/cool-thing"); + }); + + test("returns null for null input", () => { + expect(getOriginalBranchForWorkspaceForks(null)).toBeNull(); + }); + + test("returns null for empty string", () => { + expect(getOriginalBranchForWorkspaceForks("")).toBeNull(); + }); + + test("returns null for non-fork branch", () => { + expect(getOriginalBranchForWorkspaceForks("main")).toBeNull(); + expect(getOriginalBranchForWorkspaceForks("feature/my-feature")).toBeNull(); + }); + + test("returns null for branch that starts with wm-fork but has no slashes after", () => { + expect(getOriginalBranchForWorkspaceForks("wm-fork")).toBeNull(); + }); + + test("returns null when branch segment between slashes is empty", () => { + // "wm-fork//workspace" — start=8, end=8, end - start = 0 + expect(getOriginalBranchForWorkspaceForks("wm-fork//workspace")).toBeNull(); + }); +}); + +// ============================================================================= +// getWorkspaceIdForWorkspaceForkFromBranchName +// ============================================================================= + +describe("getWorkspaceIdForWorkspaceForkFromBranchName", () => { + test("extracts workspace id from valid fork branch name", () => { + expect( + getWorkspaceIdForWorkspaceForkFromBranchName("wm-fork/main/my-workspace") + ).toBe("wm-fork-my-workspace"); + }); + + test("returns null for non-fork branch", () => { + expect(getWorkspaceIdForWorkspaceForkFromBranchName("main")).toBeNull(); + expect( + getWorkspaceIdForWorkspaceForkFromBranchName("feature/my-feature") + ).toBeNull(); + }); + + test("extracts workspace id with multi-segment original branch", () => { + expect( + getWorkspaceIdForWorkspaceForkFromBranchName("wm-fork/feature/cool/ws-id") + ).toBe("wm-fork-ws-id"); + }); +}); diff --git a/cli/test/gitsync_settings_features.test.ts b/cli/test/gitsync_settings_features.test.ts index 49d8c91331..9e142e7115 100644 --- a/cli/test/gitsync_settings_features.test.ts +++ b/cli/test/gitsync_settings_features.test.ts @@ -1,4 +1,5 @@ -import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; +import { writeFile, readFile } from "node:fs/promises"; import { withTestBackend } from "./test_backend.ts"; import { shouldSkipOnCI } from "./cargo_backend.ts"; import { addWorkspace } from "../workspace.ts"; @@ -9,12 +10,7 @@ import { addWorkspace } from "../workspace.ts"; // These tests require EE features (private, enterprise) and are skipped in CI // ============================================================================= -Deno.test({ - name: "GitSync Settings: default mode writes to top-level", - ignore: shouldSkipOnCI(), // Requires EE features - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test.skipIf(shouldSkipOnCI())("GitSync Settings: default mode writes to top-level", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -44,11 +40,11 @@ Deno.test({ }); // Create initial wmill.yaml with different settings - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - f/** excludes: [] -skipVariables: false`); +skipVariables: false`, "utf-8"); // Pull with default flag const result = await backend.runCLICommand([ @@ -57,25 +53,19 @@ skipVariables: false`); '--default' ], tempDir); - assertEquals(result.code, 0, `Default mode pull should succeed: ${result.stderr}`); + expect(result.code).toEqual(0); // Read updated config - const updatedConfig = await Deno.readTextFile(`${tempDir}/wmill.yaml`); + const updatedConfig = await readFile(`${tempDir}/wmill.yaml`, "utf-8"); // Should update top-level settings, not create overrides - assertStringIncludes(updatedConfig, "includes:\n - f/special/**"); - assertStringIncludes(updatedConfig, "excludes:\n - '*.test.ts'"); - assertStringIncludes(updatedConfig, "extraIncludes:\n - g/**"); + expect(updatedConfig).toContain("includes:\n - f/special/**"); + expect(updatedConfig).toContain("excludes:\n - '*.test.ts'"); + expect(updatedConfig).toContain("extraIncludes:\n - g/**"); }); - } }); -Deno.test({ - name: "GitSync Settings: pull shows correct diff output", - ignore: shouldSkipOnCI(), // Requires EE features - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test.skipIf(shouldSkipOnCI())("GitSync Settings: pull shows correct diff output", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -105,12 +95,12 @@ Deno.test({ }); // Create wmill.yaml with different settings - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - f/** excludes: [] skipVariables: true -skipResources: false`); +skipResources: false`, "utf-8"); // Pull with diff flag const result = await backend.runCLICommand([ @@ -119,22 +109,16 @@ skipResources: false`); '--diff' ], tempDir); - assertEquals(result.code, 0, `Diff mode should succeed: ${result.stderr}`); + expect(result.code).toEqual(0); // Should show differences - assertStringIncludes(result.stdout, "Changes that would be applied locally:"); + expect(result.stdout).toContain("Changes that would be applied locally:"); // Should show the change for skipResources (ignoring ANSI color codes) - assertStringIncludes(result.stdout, "skipResources:"); + expect(result.stdout).toContain("skipResources:"); }); - } }); -Deno.test({ - name: "GitSync Settings: replace mode overwrites existing config", - ignore: shouldSkipOnCI(), // Requires EE features - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test.skipIf(shouldSkipOnCI())("GitSync Settings: replace mode overwrites existing config", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -164,12 +148,12 @@ Deno.test({ }); // Create initial wmill.yaml with settings that should be replaced - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - f/old/** excludes: - "*.old.ts" -skipVariables: true`); +skipVariables: true`, "utf-8"); // Pull with replace flag const result = await backend.runCLICommand([ @@ -178,14 +162,13 @@ skipVariables: true`); '--replace' ], tempDir); - assertEquals(result.code, 0, `Replace mode pull should succeed: ${result.stderr}`); + expect(result.code).toEqual(0); // Read updated config - const updatedConfig = await Deno.readTextFile(`${tempDir}/wmill.yaml`); + const updatedConfig = await readFile(`${tempDir}/wmill.yaml`, "utf-8"); // Should have replaced settings from backend - assertStringIncludes(updatedConfig, "f/replaced/**"); - assertStringIncludes(updatedConfig, "*.backup.ts"); + expect(updatedConfig).toContain("f/replaced/**"); + expect(updatedConfig).toContain("*.backup.ts"); }); - } }); diff --git a/cli/test/include_flags_bypass_filtering.test.ts b/cli/test/include_flags_bypass_filtering.test.ts index 897b414876..aa0fc806f4 100644 --- a/cli/test/include_flags_bypass_filtering.test.ts +++ b/cli/test/include_flags_bypass_filtering.test.ts @@ -1,4 +1,5 @@ -import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; +import { writeFile } from "node:fs/promises"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; import { parseJsonFromCLIOutput } from "./test_config_helpers.ts"; @@ -27,16 +28,12 @@ async function setupWorkspaceProfile(backend: any): Promise { // - test apps, resources, variables via seedTestData() // No additional setup needed! -Deno.test({ - name: "CLI include flags bypass restrictive path filtering", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("CLI include flags bypass restrictive path filtering", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); - + // Create wmill.yaml with very restrictive includes that would exclude special files - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "f/**" excludes: [] @@ -45,24 +42,24 @@ skipResources: true includeUsers: false includeGroups: false includeSettings: false -includeKey: false`); - +includeKey: false`, "utf-8"); + // Test: CLI flags should override config and bypass path filtering const result = await backend.runCLICommand([ - 'sync', 'pull', + 'sync', 'pull', '--include-users', - '--include-groups', + '--include-groups', '--include-settings', '--include-key', - '--dry-run', + '--dry-run', '--json-output' ], tempDir); - - assertEquals(result.code, 0, `Command failed: ${result.stderr}`); - + + expect(result.code).toEqual(0); + const output = parseJsonFromCLIOutput(result.stdout); const changePaths = output.changes.map((c: any) => c.path); - + // Assert that special files are included despite restrictive path filtering // Normalize paths for cross-platform comparison (Windows uses backslashes) const normalizedPaths = changePaths.map((p: string) => p.replace(/\\/g, '/')); @@ -70,119 +67,106 @@ includeKey: false`); const hasGroup = normalizedPaths.some((path: string) => path.includes('groups/test_group.group.yaml')); const hasSettings = changePaths.some((path: string) => path === 'settings.yaml'); const hasEncryptionKey = changePaths.some((path: string) => path === 'encryption_key.yaml'); - - assert(hasUser, `Admin user should be included despite restrictive includes. Found paths: ${normalizedPaths.join(', ')}`); - assert(hasGroup, `'test_group' should be included despite restrictive includes. Found paths: ${normalizedPaths.join(', ')}`); - assert(hasSettings, `Settings should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`); - assert(hasEncryptionKey, `Encryption key should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`); - }); -}}); -Deno.test({ - name: "CLI flags override wmill.yaml include settings", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { + expect(hasUser).toBe(true); + expect(hasGroup).toBe(true); + expect(hasSettings).toBe(true); + expect(hasEncryptionKey).toBe(true); + }); +}); + +test("CLI flags override wmill.yaml include settings", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); - + // Config explicitly disables includes, but CLI should override - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] includeUsers: false -includeGroups: false`); - +includeGroups: false`, "utf-8"); + // CLI flags should override config file settings const result = await backend.runCLICommand([ 'sync', 'pull', '--include-users', - '--include-groups', + '--include-groups', '--dry-run', '--json-output' ], tempDir); - - assertEquals(result.code, 0, `Command failed: ${result.stderr}`); - + + expect(result.code).toEqual(0); + const output = parseJsonFromCLIOutput(result.stdout); const changePaths = output.changes.map((c: any) => c.path); - + // Normalize paths for cross-platform comparison (Windows uses backslashes) const normalizedPaths = changePaths.map((p: string) => p.replace(/\\/g, '/')); const hasUser = normalizedPaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml')); const hasGroup = normalizedPaths.some((path: string) => path.includes('groups/test_group.group.yaml')); - assert(hasUser, `CLI --include-users should override config includeUsers: false. Found paths: ${normalizedPaths.join(', ')}`); - assert(hasGroup, `CLI --include-groups should override config includeGroups: false. Found paths: ${normalizedPaths.join(', ')}`); + expect(hasUser).toBe(true); + expect(hasGroup).toBe(true); }); -}}); +}); -Deno.test({ - name: "Skip flags work correctly with getTypeStrFromPath and lock files", - ignore: true, // TODO: Requires backend app creation to work (currently failing with v2_job_queue constraint) - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Skip flags work correctly with getTypeStrFromPath and lock files", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); - + // Create wmill.yaml with skip flags enabled - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] skipScripts: true skipFlows: false -includeUsers: true`); - +includeUsers: true`, "utf-8"); + const result = await backend.runCLICommand([ 'sync', 'pull', '--dry-run', '--json-output' ], tempDir); - - assertEquals(result.code, 0, `Command failed: ${result.stderr}`); - + + expect(result.code).toEqual(0); + const output = parseJsonFromCLIOutput(result.stdout); const changePaths = output.changes.map((c: any) => c.path); - + // Scripts should be skipped (including lock files) - the backend doesn't create scripts by default - const hasScript = changePaths.some((path: string) => + const hasScript = changePaths.some((path: string) => path.endsWith('.py') || path.endsWith('.ts') || path.endsWith('.go') || path.endsWith('.sh') ); const hasScriptLock = changePaths.some((path: string) => path.endsWith('.script.lock')); - + // Apps should be included (the backend creates test apps) const hasApp = changePaths.some((path: string) => path.includes('test_dashboard') || path.endsWith('.app.yaml')); - + // Users should still be included const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml')); - - assert(!hasScript, `Standalone scripts should be skipped when skipScripts: true. Found paths: ${changePaths.join(', ')}`); - assert(!hasScriptLock, `Script lock files should be skipped when skipScripts: true. Found paths: ${changePaths.join(', ')}`); - assert(hasApp, `Apps should be included (inline scripts are part of apps). Found paths: ${changePaths.join(', ')}`); - assert(hasUser, `Users should be included when includeUsers: true. Found paths: ${changePaths.join(', ')}`); - }); -}}); -Deno.test({ - name: "Mixed include and skip flags work together", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { + expect(hasScript).toBe(false); + expect(hasScriptLock).toBe(false); + expect(hasApp).toBe(true); + expect(hasUser).toBe(true); + }); +}); + +test("Mixed include and skip flags work together", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); - + // Create restrictive config with mixed settings - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "f/**" excludes: [] skipScripts: true includeUsers: false -includeSettings: false`); - +includeSettings: false`, "utf-8"); + const result = await backend.runCLICommand([ 'sync', 'pull', '--skip-scripts', // Reinforce script skipping @@ -190,25 +174,25 @@ includeSettings: false`); '--dry-run', '--json-output' ], tempDir); - - assertEquals(result.code, 0, `Command failed: ${result.stderr}`); - + + expect(result.code).toEqual(0); + const output = parseJsonFromCLIOutput(result.stdout); const changePaths = output.changes.map((c: any) => c.path); - + // Scripts should be excluded - const hasScript = changePaths.some((path: string) => + const hasScript = changePaths.some((path: string) => path.endsWith('.py') || path.endsWith('.ts') || path.endsWith('.go') || path.endsWith('.sh') ); - + // Users should be included (CLI override) const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml')); - + // Settings should be excluded (no CLI override, restrictive path filtering) const hasSettings = changePaths.some((path: string) => path === 'settings.yaml'); - - assert(!hasScript, `Scripts should be excluded due to skipScripts. Found paths: ${changePaths.join(', ')}`); - assert(hasUser, `Users should be included due to CLI --include-users override. Found paths: ${changePaths.join(', ')}`); - assert(!hasSettings, `Settings should be excluded (no CLI override + restrictive paths). Found paths: ${changePaths.join(', ')}`); + + expect(hasScript).toBe(false); + expect(hasUser).toBe(true); + expect(hasSettings).toBe(false); }); -}}); \ No newline at end of file +}); diff --git a/cli/test/init_no_git_sync.test.ts b/cli/test/init_no_git_sync.test.ts index 89551f13a8..a5914cd7ce 100644 --- a/cli/test/init_no_git_sync.test.ts +++ b/cli/test/init_no_git_sync.test.ts @@ -3,7 +3,8 @@ * This creates a unit test that directly tests the logic without needing a backend */ -import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; +import { readFile } from "node:fs/promises"; import { DEFAULT_SYNC_OPTIONS } from "../src/core/conf.ts"; import { withTestBackend } from "./test_backend.ts"; import { shouldSkipOnCI } from "./cargo_backend.ts"; @@ -36,55 +37,50 @@ function createWorkspaceProfileNoRepos(workspace: any): any { return workspaceProfile; } -Deno.test("Init: createWorkspaceProfile includes defaults when no repositories exist", () => { - console.log('🧪 Testing init logic for workspace with no git-sync repositories...'); - +test("Init: createWorkspaceProfile includes defaults when no repositories exist", () => { + console.log('Testing init logic for workspace with no git-sync repositories...'); + const workspaceProfile = createWorkspaceProfileNoRepos(mockWorkspace); - + console.log('Generated workspace profile:', JSON.stringify(workspaceProfile, null, 2)); - + // Verify basic workspace info - assertEquals(workspaceProfile.baseUrl, 'https://app.windmill.dev/'); - assertEquals(workspaceProfile.workspaceId, 'test-workspace'); - + expect(workspaceProfile.baseUrl).toEqual('https://app.windmill.dev/'); + expect(workspaceProfile.workspaceId).toEqual('test-workspace'); + // Verify default sync settings are included - assert(Array.isArray(workspaceProfile.includes), 'Should have includes array'); - assertEquals(workspaceProfile.includes.length, 1, 'Should have one include pattern'); - assertEquals(workspaceProfile.includes[0], 'f/**', 'Should include f/** pattern'); - - assert(Array.isArray(workspaceProfile.excludes), 'Should have excludes array'); - assertEquals(workspaceProfile.excludes.length, 0, 'Should have empty excludes array'); - - assertEquals(workspaceProfile.defaultTs, 'bun', 'Should have bun as default TypeScript runtime'); - - console.log('✅ Workspace profile correctly includes default sync settings when no repositories exist'); + expect(Array.isArray(workspaceProfile.includes)).toBeTruthy(); + expect(workspaceProfile.includes.length).toEqual(1); + expect(workspaceProfile.includes[0]).toEqual('f/**'); + + expect(Array.isArray(workspaceProfile.excludes)).toBeTruthy(); + expect(workspaceProfile.excludes.length).toEqual(0); + + expect(workspaceProfile.defaultTs).toEqual('bun'); + + console.log('Workspace profile correctly includes default sync settings when no repositories exist'); }); -Deno.test("Init: verify DEFAULT_SYNC_OPTIONS has expected values", () => { - console.log('🔍 Verifying DEFAULT_SYNC_OPTIONS contains expected values...'); - +test("Init: verify DEFAULT_SYNC_OPTIONS has expected values", () => { + console.log('Verifying DEFAULT_SYNC_OPTIONS contains expected values...'); + console.log('DEFAULT_SYNC_OPTIONS:', JSON.stringify(DEFAULT_SYNC_OPTIONS, null, 2)); - + // Verify the default options include the expected f/** pattern - assert(Array.isArray(DEFAULT_SYNC_OPTIONS.includes), 'DEFAULT_SYNC_OPTIONS should have includes array'); - assertEquals(DEFAULT_SYNC_OPTIONS.includes.length, 1, 'Should have one include pattern'); - assertEquals(DEFAULT_SYNC_OPTIONS.includes[0], 'f/**', 'Should default to f/** pattern'); - - assert(Array.isArray(DEFAULT_SYNC_OPTIONS.excludes), 'DEFAULT_SYNC_OPTIONS should have excludes array'); - assertEquals(DEFAULT_SYNC_OPTIONS.excludes.length, 0, 'Should have empty excludes array by default'); - - assertEquals(DEFAULT_SYNC_OPTIONS.defaultTs, 'bun', 'Should default to bun runtime'); - - console.log('✅ DEFAULT_SYNC_OPTIONS has expected values'); + expect(Array.isArray(DEFAULT_SYNC_OPTIONS.includes)).toBeTruthy(); + expect(DEFAULT_SYNC_OPTIONS.includes.length).toEqual(1); + expect(DEFAULT_SYNC_OPTIONS.includes[0]).toEqual('f/**'); + + expect(Array.isArray(DEFAULT_SYNC_OPTIONS.excludes)).toBeTruthy(); + expect(DEFAULT_SYNC_OPTIONS.excludes.length).toEqual(0); + + expect(DEFAULT_SYNC_OPTIONS.defaultTs).toEqual('bun'); + + console.log('DEFAULT_SYNC_OPTIONS has expected values'); }); -Deno.test({ - name: "Init: --use-backend flag applies git-sync settings", - ignore: shouldSkipOnCI(), // Requires EE features - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { - await withTestBackend(async (backend, tempDir) => { +test.skipIf(shouldSkipOnCI())("Init: --use-backend flag applies git-sync settings", async () => { + await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { remote: backend.baseUrl, @@ -122,29 +118,23 @@ Deno.test({ '--repository', 'u/test/init_repo' ], tempDir); - assertEquals(result.code, 0, `Init with --use-backend should succeed: ${result.stderr}`); + expect(result.code).toEqual(0); // Verify wmill.yaml was created with backend settings - const wmillYaml = await Deno.readTextFile(`${tempDir}/wmill.yaml`); - + const wmillYaml = await readFile(`${tempDir}/wmill.yaml`, "utf-8"); + // Should have backend-applied settings written to top-level (not overrides) - assertStringIncludes(wmillYaml, "f/backend/**", "Should include backend's include_path"); - assertStringIncludes(wmillYaml, "*.test.ts", "Should include backend's exclude_path"); - assertStringIncludes(wmillYaml, "g/**", "Should include backend's extra_include_path"); - + expect(wmillYaml).toContain("f/backend/**"); + expect(wmillYaml).toContain("*.test.ts"); + expect(wmillYaml).toContain("g/**"); + // Should have empty overrides section for consistency - assertStringIncludes(wmillYaml, "gitBranches: {}"); - }); - } + expect(wmillYaml).toContain("gitBranches: {}"); + }); }); -Deno.test({ - name: "Init: --use-default bypasses backend settings check", - ignore: shouldSkipOnCI(), // Requires EE features - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { - await withTestBackend(async (backend, tempDir) => { +test.skipIf(shouldSkipOnCI())("Init: --use-default bypasses backend settings check", async () => { + await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { remote: backend.baseUrl, @@ -181,18 +171,17 @@ Deno.test({ '--use-default' ], tempDir); - assertEquals(result.code, 0, `Init with --use-default should succeed: ${result.stderr}`); + expect(result.code).toEqual(0); // Verify wmill.yaml was created with default settings only - const wmillYaml = await Deno.readTextFile(`${tempDir}/wmill.yaml`); - + const wmillYaml = await readFile(`${tempDir}/wmill.yaml`, "utf-8"); + // Should have default settings, not backend settings - assertStringIncludes(wmillYaml, "includes:\n - f/**", "Should use default includes"); - assertStringIncludes(wmillYaml, "defaultTs: bun", "Should use default TypeScript runtime"); - + expect(wmillYaml).toContain("includes:\n - f/**"); + expect(wmillYaml).toContain("defaultTs: bun"); + // Should NOT have backend-specific settings - assertEquals(wmillYaml.includes("f/should-be-ignored/**"), false, "Should not include backend settings"); - assertStringIncludes(wmillYaml, "gitBranches: {}", "Should have empty overrides section for consistency"); - }); - } -}); \ No newline at end of file + expect(wmillYaml.includes("f/should-be-ignored/**")).toEqual(false); + expect(wmillYaml).toContain("gitBranches: {}"); + }); +}); diff --git a/cli/test/lint_command.test.ts b/cli/test/lint_command.test.ts index f7871c7034..6b3d91244e 100644 --- a/cli/test/lint_command.test.ts +++ b/cli/test/lint_command.test.ts @@ -1,8 +1,7 @@ -import { - assert, - assertEquals, - assertStringIncludes, -} from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; +import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises"; +import os from "node:os"; +import * as path from "@std/path"; import { formatValidationError, runLint, @@ -11,30 +10,31 @@ import { async function withTempDir( fn: (tempDir: string) => Promise, ): Promise { - const tempDir = await Deno.makeTempDir({ prefix: "wmill_lint_test_" }); - const originalCwd = Deno.cwd(); + const tempDir = await mkdtemp(path.join(os.tmpdir(), "wmill_lint_test_")); + const originalCwd = process.cwd(); try { - Deno.chdir(tempDir); + process.chdir(tempDir); await fn(tempDir); } finally { - Deno.chdir(originalCwd); - await Deno.remove(tempDir, { recursive: true }); + process.chdir(originalCwd); + await rm(tempDir, { recursive: true }); } } -Deno.test("lint: validates flow, schedule, and trigger yaml files", async () => { +test("lint: validates flow, schedule, and trigger yaml files", async () => { await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true }); - await Deno.writeTextFile( + await mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true }); + await writeFile( `${tempDir}/f/my_flow.flow/flow.yaml`, `summary: My flow value: modules: [] `, + "utf-8" ); - await Deno.mkdir(`${tempDir}/f/jobs`, { recursive: true }); - await Deno.writeTextFile( + await mkdir(`${tempDir}/f/jobs`, { recursive: true }); + await writeFile( `${tempDir}/f/jobs/daily.schedule.yaml`, `schedule: "0 0 12 * * *" timezone: "UTC" @@ -42,10 +42,11 @@ enabled: true script_path: "f/jobs/daily_sync" is_flow: false `, + "utf-8" ); - await Deno.mkdir(`${tempDir}/f/triggers`, { recursive: true }); - await Deno.writeTextFile( + await mkdir(`${tempDir}/f/triggers`, { recursive: true }); + await writeFile( `${tempDir}/f/triggers/hook.http_trigger.yaml`, `script_path: "f/triggers/http_handler" is_flow: false @@ -58,93 +59,97 @@ workspaced_route: false wrap_body: false raw_string: false `, + "utf-8" ); - await Deno.writeTextFile( + await writeFile( `${tempDir}/f/triggers/inbox.email_trigger.yaml`, `script_path: "f/triggers/email_handler" is_flow: false local_part: "inbox" `, + "utf-8" ); const report = await runLint({} as any, tempDir); - assertEquals(report.exitCode, 0); - assertEquals(report.validatedFiles, 4); - assertEquals(report.validFiles, 4); - assertEquals(report.invalidFiles, 0); - assertEquals(report.warnings.length, 0); + expect(report.exitCode).toEqual(0); + expect(report.validatedFiles).toEqual(4); + expect(report.validFiles).toEqual(4); + expect(report.invalidFiles).toEqual(0); + expect(report.warnings.length).toEqual(0); }); }); -Deno.test("lint: returns errors for invalid schedule documents", async () => { +test("lint: returns errors for invalid schedule documents", async () => { await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/jobs`, { recursive: true }); - await Deno.writeTextFile( + await mkdir(`${tempDir}/f/jobs`, { recursive: true }); + await writeFile( `${tempDir}/f/jobs/broken.schedule.yaml`, `timezone: "UTC" enabled: true script_path: "f/jobs/broken" is_flow: false `, + "utf-8" ); const report = await runLint({} as any, tempDir); - assertEquals(report.exitCode, 1); - assertEquals(report.validatedFiles, 1); - assertEquals(report.invalidFiles, 1); - assertEquals(report.issues[0].path, "f/jobs/broken.schedule.yaml"); - assert( + expect(report.exitCode).toEqual(1); + expect(report.validatedFiles).toEqual(1); + expect(report.invalidFiles).toEqual(1); + expect(report.issues[0].path).toEqual("f/jobs/broken.schedule.yaml"); + expect( report.issues[0].errors.some((message) => message.includes("missing required property 'schedule'") ), - ); + ).toBeTruthy(); }); }); -Deno.test("lint: warns and skips unsupported native trigger schemas", async () => { +test("lint: warns and skips unsupported native trigger schemas", async () => { await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/triggers`, { recursive: true }); - await Deno.writeTextFile( + await mkdir(`${tempDir}/f/triggers`, { recursive: true }); + await writeFile( `${tempDir}/f/triggers/webhook.script.123.nextcloud_native_trigger.yaml`, `path: "f/triggers/native" `, + "utf-8" ); const report = await runLint({} as any, tempDir); - assertEquals(report.exitCode, 0); - assertEquals(report.validatedFiles, 0); - assertEquals(report.skippedUnsupportedFiles, 1); - assertEquals(report.warnings.length, 1); - assertStringIncludes( + expect(report.exitCode).toEqual(0); + expect(report.validatedFiles).toEqual(0); + expect(report.skippedUnsupportedFiles).toEqual(1); + expect(report.warnings.length).toEqual(1); + expect( report.warnings[0].message, - "Unsupported trigger schema", - ); + ).toContain("Unsupported trigger schema"); const failOnWarnReport = await runLint( { failOnWarn: true } as any, tempDir, ); - assertEquals(failOnWarnReport.exitCode, 1); + expect(failOnWarnReport.exitCode).toEqual(1); }); }); -Deno.test("lint: uses wmill.yaml include filters for file discovery", async () => { +test("lint: uses wmill.yaml include filters for file discovery", async () => { await withTempDir(async (tempDir) => { - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "f/allowed/**" excludes: [] `, + "utf-8" ); - await Deno.mkdir(`${tempDir}/f/allowed`, { recursive: true }); - await Deno.writeTextFile( + await mkdir(`${tempDir}/f/allowed`, { recursive: true }); + await writeFile( `${tempDir}/f/allowed/ok.schedule.yaml`, `schedule: "0 0 12 * * *" timezone: "UTC" @@ -152,113 +157,109 @@ enabled: true script_path: "f/jobs/ok" is_flow: false `, + "utf-8" ); - await Deno.mkdir(`${tempDir}/f/blocked`, { recursive: true }); - await Deno.writeTextFile( + await mkdir(`${tempDir}/f/blocked`, { recursive: true }); + await writeFile( `${tempDir}/f/blocked/bad.schedule.yaml`, `timezone: "UTC" enabled: true script_path: "f/jobs/bad" is_flow: false `, + "utf-8" ); const report = await runLint({} as any, tempDir); - assertEquals(report.exitCode, 0); - assertEquals(report.validatedFiles, 1); - assertEquals(report.validFiles, 1); - assertEquals(report.invalidFiles, 0); - assertEquals(report.issues.length, 0); + expect(report.exitCode).toEqual(0); + expect(report.validatedFiles).toEqual(1); + expect(report.validFiles).toEqual(1); + expect(report.invalidFiles).toEqual(0); + expect(report.issues.length).toEqual(0); }); }); // --- formatValidationError unit tests --- -Deno.test("formatValidationError: required keyword", () => { - assertEquals( +test("formatValidationError: required keyword", () => { + expect( formatValidationError({ instancePath: "/value", keyword: "required", message: "must have required property 'modules'", params: { missingProperty: "modules" }, }), - "/value missing required property 'modules'", - ); + ).toEqual("/value missing required property 'modules'"); }); -Deno.test("formatValidationError: additionalProperties keyword", () => { - assertEquals( +test("formatValidationError: additionalProperties keyword", () => { + expect( formatValidationError({ instancePath: "/value", keyword: "additionalProperties", message: "must NOT have additional properties", params: { additionalProperty: "typo_field" }, }), - "/value has unknown property 'typo_field'", - ); + ).toEqual("/value has unknown property 'typo_field'"); }); -Deno.test("formatValidationError: enum keyword filters null values", () => { - assertEquals( +test("formatValidationError: enum keyword filters null values", () => { + expect( formatValidationError({ instancePath: "/http_method", keyword: "enum", message: "must be equal to one of the allowed values", params: { allowedValues: [null, "get", "post", "put"] }, }), - "/http_method must be one of: 'get', 'post', 'put'", - ); + ).toEqual("/http_method must be one of: 'get', 'post', 'put'"); }); -Deno.test("formatValidationError: falls back to message", () => { - assertEquals( +test("formatValidationError: falls back to message", () => { + expect( formatValidationError({ instancePath: "/timeout", keyword: "type", message: "must be integer", }), - "/timeout must be integer", - ); + ).toEqual("/timeout must be integer"); }); -Deno.test("formatValidationError: uses / for empty instancePath", () => { - assertEquals( +test("formatValidationError: uses / for empty instancePath", () => { + expect( formatValidationError({ instancePath: "", keyword: "required", message: "must have required property 'summary'", params: { missingProperty: "summary" }, }), - "/ missing required property 'summary'", - ); + ).toEqual("/ missing required property 'summary'"); }); -Deno.test("formatValidationError: generic fallback when no message", () => { - assertEquals( +test("formatValidationError: generic fallback when no message", () => { + expect( formatValidationError({ instancePath: "/field", keyword: "custom" }), - "/field validation error", - ); + ).toEqual("/field validation error"); }); // --- runLint integration tests --- -Deno.test("lint: throws for non-existent directory", async () => { +test("lint: throws for non-existent directory", async () => { let threw = false; try { await runLint({} as any, "/tmp/wmill_lint_nonexistent_" + Date.now()); } catch (e) { threw = true; - assertStringIncludes((e as Error).message, "Directory not found"); + expect((e as Error).message).toContain("Directory not found"); } - assert(threw, "Expected runLint to throw for non-existent directory"); + expect(threw).toBeTruthy(); }); -Deno.test("lint: json-shaped report contains all fields", async () => { +test("lint: json-shaped report contains all fields", async () => { await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/jobs`, { recursive: true }); - await Deno.writeTextFile( + await mkdir(`${tempDir}/f/jobs`, { recursive: true }); + await writeFile( `${tempDir}/f/jobs/ok.schedule.yaml`, `schedule: "0 0 * * *" timezone: "UTC" @@ -266,33 +267,34 @@ enabled: true script_path: "f/jobs/ok" is_flow: false `, + "utf-8" ); const report = await runLint({ json: true } as any, tempDir); // Verify the report object has the shape expected by --json output - assertEquals(typeof report.scannedFiles, "number"); - assertEquals(typeof report.validatedFiles, "number"); - assertEquals(typeof report.validFiles, "number"); - assertEquals(typeof report.invalidFiles, "number"); - assertEquals(typeof report.skippedUnsupportedFiles, "number"); - assert(Array.isArray(report.warnings)); - assert(Array.isArray(report.issues)); - assertEquals(typeof report.success, "boolean"); - assertEquals(typeof report.exitCode, "number"); + expect(typeof report.scannedFiles).toEqual("number"); + expect(typeof report.validatedFiles).toEqual("number"); + expect(typeof report.validFiles).toEqual("number"); + expect(typeof report.invalidFiles).toEqual("number"); + expect(typeof report.skippedUnsupportedFiles).toEqual("number"); + expect(Array.isArray(report.warnings)).toBeTruthy(); + expect(Array.isArray(report.issues)).toBeTruthy(); + expect(typeof report.success).toEqual("boolean"); + expect(typeof report.exitCode).toEqual("number"); // JSON.stringify should round-trip cleanly const json = JSON.parse(JSON.stringify(report)); - assertEquals(json.success, true); - assertEquals(json.exitCode, 0); + expect(json.success).toEqual(true); + expect(json.exitCode).toEqual(0); }); }); -Deno.test("lint: --fail-on-warn with mixed valid and warning files", async () => { +test("lint: --fail-on-warn with mixed valid and warning files", async () => { await withTempDir(async (tempDir) => { // A valid schedule - await Deno.mkdir(`${tempDir}/f/jobs`, { recursive: true }); - await Deno.writeTextFile( + await mkdir(`${tempDir}/f/jobs`, { recursive: true }); + await writeFile( `${tempDir}/f/jobs/ok.schedule.yaml`, `schedule: "0 0 * * *" timezone: "UTC" @@ -300,36 +302,38 @@ enabled: true script_path: "f/jobs/ok" is_flow: false `, + "utf-8" ); // An unsupported native trigger that produces a warning - await Deno.mkdir(`${tempDir}/f/triggers`, { recursive: true }); - await Deno.writeTextFile( + await mkdir(`${tempDir}/f/triggers`, { recursive: true }); + await writeFile( `${tempDir}/f/triggers/webhook.script.123.nextcloud_native_trigger.yaml`, `path: "f/triggers/native" `, + "utf-8" ); // Without --fail-on-warn: passes const normalReport = await runLint({} as any, tempDir); - assertEquals(normalReport.exitCode, 0); - assertEquals(normalReport.success, true); - assertEquals(normalReport.validFiles, 1); - assertEquals(normalReport.warnings.length, 1); + expect(normalReport.exitCode).toEqual(0); + expect(normalReport.success).toEqual(true); + expect(normalReport.validFiles).toEqual(1); + expect(normalReport.warnings.length).toEqual(1); // With --fail-on-warn: fails due to warning const strictReport = await runLint({ failOnWarn: true } as any, tempDir); - assertEquals(strictReport.exitCode, 1); - assertEquals(strictReport.success, false); - assertEquals(strictReport.validFiles, 1); - assertEquals(strictReport.warnings.length, 1); + expect(strictReport.exitCode).toEqual(1); + expect(strictReport.success).toEqual(false); + expect(strictReport.validFiles).toEqual(1); + expect(strictReport.warnings.length).toEqual(1); }); }); -Deno.test("lint: reports enum errors with allowed values for invalid trigger", async () => { +test("lint: reports enum errors with allowed values for invalid trigger", async () => { await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/triggers`, { recursive: true }); - await Deno.writeTextFile( + await mkdir(`${tempDir}/f/triggers`, { recursive: true }); + await writeFile( `${tempDir}/f/triggers/hook.http_trigger.yaml`, `script_path: "f/triggers/http_handler" is_flow: false @@ -341,14 +345,14 @@ workspaced_route: false wrap_body: false raw_string: false `, + "utf-8" ); const report = await runLint({} as any, tempDir); - assertEquals(report.invalidFiles, 1); - assert( + expect(report.invalidFiles).toEqual(1); + expect( report.issues[0].errors.some((msg) => msg.includes("must be one of:")), - `Expected 'must be one of' error but got: ${report.issues[0].errors}`, - ); + ).toBeTruthy(); }); }); diff --git a/cli/test/lint_locks.test.ts b/cli/test/lint_locks.test.ts new file mode 100644 index 0000000000..742e2318df --- /dev/null +++ b/cli/test/lint_locks.test.ts @@ -0,0 +1,331 @@ +import { expect, test, describe } from "bun:test"; +import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises"; +import os from "node:os"; +import * as path from "@std/path"; +import { checkMissingLocks, runLint } from "../src/commands/lint/lint.ts"; + +async function withTempDir( + fn: (tempDir: string) => Promise, +): Promise { + const tempDir = await mkdtemp(path.join(os.tmpdir(), "wmill_lint_locks_")); + const originalCwd = process.cwd(); + try { + process.chdir(tempDir); + await fn(tempDir); + } finally { + process.chdir(originalCwd); + await rm(tempDir, { recursive: true }); + } +} + +// Helper to create a script with metadata and optional lock +async function createScript( + tempDir: string, + scriptBase: string, + ext: string, + opts: { lock?: string; lockFileContent?: string } = {}, +) { + const dir = path.dirname(path.join(tempDir, scriptBase)); + await mkdir(dir, { recursive: true }); + + // Script content file + await writeFile(path.join(tempDir, scriptBase + ext), "# placeholder", "utf-8"); + + // Metadata YAML + const lockLine = opts.lock !== undefined ? `lock: "${opts.lock}"` : "lock: ''"; + await writeFile( + path.join(tempDir, scriptBase + ".script.yaml"), + `summary: test\n${lockLine}\nschema:\n properties: {}\n`, + "utf-8", + ); + + // Lock file (if inline reference) + if (opts.lockFileContent !== undefined) { + await writeFile( + path.join(tempDir, scriptBase + ".script.lock"), + opts.lockFileContent, + "utf-8", + ); + } +} + +// --- checkMissingLocks unit tests --- + +describe("checkMissingLocks", () => { + test("reports missing lock for python script", async () => { + await withTempDir(async (tempDir) => { + await createScript(tempDir, "f/my_script", ".py", { lock: "" }); + + const issues = await checkMissingLocks({} as any, tempDir); + + expect(issues.length).toBe(1); + expect(issues[0].target).toBe("script"); + expect(issues[0].errors[0]).toContain("Missing lock"); + expect(issues[0].errors[0]).toContain("python3"); + }); + }); + + test("no issues for python script with inline lock file", async () => { + await withTempDir(async (tempDir) => { + await createScript(tempDir, "f/my_script", ".py", { + lock: "!inline f/my_script.script.lock", + lockFileContent: "some-dep==1.0.0", + }); + + const issues = await checkMissingLocks({} as any, tempDir); + + expect(issues.length).toBe(0); + }); + }); + + test("reports missing lock when inline lock file is empty", async () => { + await withTempDir(async (tempDir) => { + await createScript(tempDir, "f/my_script", ".py", { + lock: "!inline f/my_script.script.lock", + lockFileContent: "", + }); + + const issues = await checkMissingLocks({} as any, tempDir); + + expect(issues.length).toBe(1); + expect(issues[0].errors[0]).toContain("Missing lock"); + }); + }); + + test("no issues for bash script without lock (lock not required)", async () => { + await withTempDir(async (tempDir) => { + await createScript(tempDir, "f/my_bash", ".sh", { lock: "" }); + + const issues = await checkMissingLocks({} as any, tempDir); + + expect(issues.length).toBe(0); + }); + }); + + test("reports missing lock for bun script", async () => { + await withTempDir(async (tempDir) => { + await createScript(tempDir, "f/my_ts", ".ts", { lock: "" }); + + const issues = await checkMissingLocks( + { defaultTs: "bun" } as any, + tempDir, + ); + + expect(issues.length).toBe(1); + expect(issues[0].errors[0]).toContain("Missing lock"); + expect(issues[0].errors[0]).toContain("bun"); + }); + }); + + test("reports missing lock for flow inline rawscript", async () => { + await withTempDir(async (tempDir) => { + await mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true }); + await writeFile( + `${tempDir}/f/my_flow.flow/flow.yaml`, + `summary: test flow +value: + modules: + - id: step1 + value: + type: rawscript + language: python3 + content: "print('hello')" +`, + "utf-8", + ); + + const issues = await checkMissingLocks({} as any, tempDir); + + expect(issues.length).toBe(1); + expect(issues[0].target).toBe("flow_inline_script"); + expect(issues[0].errors[0]).toContain("step1"); + expect(issues[0].errors[0]).toContain("python3"); + }); + }); + + test("no issues for flow inline rawscript with lock", async () => { + await withTempDir(async (tempDir) => { + await mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true }); + await writeFile( + `${tempDir}/f/my_flow.flow/flow.yaml`, + `summary: test flow +value: + modules: + - id: step1 + value: + type: rawscript + language: python3 + content: "print('hello')" + lock: "some-dep==1.0.0" +`, + "utf-8", + ); + + const issues = await checkMissingLocks({} as any, tempDir); + + expect(issues.length).toBe(0); + }); + }); + + test("reports missing lock for nested flow modules (forloopflow)", async () => { + await withTempDir(async (tempDir) => { + await mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true }); + await writeFile( + `${tempDir}/f/my_flow.flow/flow.yaml`, + `summary: test flow +value: + modules: + - id: loop1 + value: + type: forloopflow + modules: + - id: inner_step + value: + type: rawscript + language: python3 + content: "print('inner')" +`, + "utf-8", + ); + + const issues = await checkMissingLocks({} as any, tempDir); + + expect(issues.length).toBe(1); + expect(issues[0].errors[0]).toContain("inner_step"); + }); + }); + + test("reports missing lock for app inline script", async () => { + await withTempDir(async (tempDir) => { + await mkdir(`${tempDir}/f/my_app.app`, { recursive: true }); + await writeFile( + `${tempDir}/f/my_app.app/app.yaml`, + `value: + grid: + - data: + inlineScript: + language: python3 + content: "x = 1" +`, + "utf-8", + ); + + const issues = await checkMissingLocks({} as any, tempDir); + + expect(issues.length).toBe(1); + expect(issues[0].target).toBe("app_inline_script"); + expect(issues[0].errors[0]).toContain("python3"); + }); + }); + + test("no issues for app inline script with lock", async () => { + await withTempDir(async (tempDir) => { + await mkdir(`${tempDir}/f/my_app.app`, { recursive: true }); + await writeFile( + `${tempDir}/f/my_app.app/app.yaml`, + `value: + grid: + - data: + inlineScript: + language: python3 + content: "x = 1" + lock: "some-dep==1.0.0" +`, + "utf-8", + ); + + const issues = await checkMissingLocks({} as any, tempDir); + + expect(issues.length).toBe(0); + }); + }); + + test("no issues for flow with non-lock-requiring language (bash)", async () => { + await withTempDir(async (tempDir) => { + await mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true }); + await writeFile( + `${tempDir}/f/my_flow.flow/flow.yaml`, + `summary: test flow +value: + modules: + - id: step1 + value: + type: rawscript + language: bash + content: "echo hello" +`, + "utf-8", + ); + + const issues = await checkMissingLocks({} as any, tempDir); + + expect(issues.length).toBe(0); + }); + }); + + test("skips raw app without backend folder", async () => { + await withTempDir(async (tempDir) => { + await mkdir(`${tempDir}/f/my_rawapp.raw_app`, { recursive: true }); + await writeFile( + `${tempDir}/f/my_rawapp.raw_app/raw_app.yaml`, + `summary: test raw app +`, + "utf-8", + ); + // No backend/ folder created + + const issues = await checkMissingLocks({} as any, tempDir); + + expect(issues.length).toBe(0); + }); + }); +}); + +// --- runLint --locks-required integration tests --- + +describe("runLint with --locks-required", () => { + test("reports lock issues when locksRequired is true", async () => { + await withTempDir(async (tempDir) => { + await createScript(tempDir, "f/my_script", ".py", { lock: "" }); + + const report = await runLint({ locksRequired: true } as any, tempDir); + + expect(report.success).toBe(false); + expect(report.exitCode).toBe(1); + expect(report.issues.length).toBeGreaterThanOrEqual(1); + expect( + report.issues.some((i) => i.errors.some((e) => e.includes("Missing lock"))), + ).toBe(true); + }); + }); + + test("does not check locks when locksRequired is false", async () => { + await withTempDir(async (tempDir) => { + await createScript(tempDir, "f/my_script", ".py", { lock: "" }); + + const report = await runLint({} as any, tempDir); + + // Without locksRequired, no lock issues should appear + expect( + report.issues.some((i) => i.errors.some((e) => e.includes("Missing lock"))), + ).toBe(false); + }); + }); + + test("passes when locksRequired is true and locks exist", async () => { + await withTempDir(async (tempDir) => { + await createScript(tempDir, "f/my_script", ".py", { + lock: "!inline f/my_script.script.lock", + lockFileContent: "some-dep==1.0.0", + }); + + const report = await runLint({ locksRequired: true } as any, tempDir); + + expect(report.success).toBe(true); + expect(report.exitCode).toBe(0); + expect( + report.issues.some((i) => i.errors.some((e) => e.includes("Missing lock"))), + ).toBe(false); + }); + }); +}); diff --git a/cli/test/local_encryption_unit.test.ts b/cli/test/local_encryption_unit.test.ts new file mode 100644 index 0000000000..a83d230e43 --- /dev/null +++ b/cli/test/local_encryption_unit.test.ts @@ -0,0 +1,94 @@ +/** + * Unit tests for local_encryption.ts encrypt/decrypt functions. + * Tests round-trip encryption, different key lengths, and error handling. + */ + +import { expect, test, describe } from "bun:test"; +import { encrypt, decrypt } from "../src/utils/local_encryption.ts"; + +// ============================================================================= +// encrypt / decrypt round-trip +// ============================================================================= + +describe("encrypt and decrypt", () => { + test("round-trip with a simple message", async () => { + const key = "my-secret-key"; + const message = "Hello, World!"; + const encrypted = await encrypt(message, key); + const decrypted = await decrypt(encrypted, key); + expect(decrypted).toBe(message); + }); + + test("round-trip with empty string", async () => { + const key = "key"; + const encrypted = await encrypt("", key); + const decrypted = await decrypt(encrypted, key); + expect(decrypted).toBe(""); + }); + + test("round-trip with long message", async () => { + const key = "test-key-123"; + const message = "A".repeat(10000); + const encrypted = await encrypt(message, key); + const decrypted = await decrypt(encrypted, key); + expect(decrypted).toBe(message); + }); + + test("round-trip with unicode characters", async () => { + const key = "unicode-key"; + const message = "Hello 🌍 世界 مرحبا"; + const encrypted = await encrypt(message, key); + const decrypted = await decrypt(encrypted, key); + expect(decrypted).toBe(message); + }); + + test("round-trip with very short key", async () => { + const key = "k"; + const message = "short key test"; + const encrypted = await encrypt(message, key); + const decrypted = await decrypt(encrypted, key); + expect(decrypted).toBe(message); + }); + + test("round-trip with very long key", async () => { + const key = "x".repeat(1000); + const message = "long key test"; + const encrypted = await encrypt(message, key); + const decrypted = await decrypt(encrypted, key); + expect(decrypted).toBe(message); + }); + + test("encrypted output is base64", async () => { + const encrypted = await encrypt("test", "key"); + // base64 characters: A-Z, a-z, 0-9, +, /, = + expect(encrypted).toMatch(/^[A-Za-z0-9+/=]+$/); + }); + + test("same message encrypted twice produces different ciphertexts (random IV)", async () => { + const key = "determinism-test"; + const message = "same input"; + const enc1 = await encrypt(message, key); + const enc2 = await encrypt(message, key); + expect(enc1).not.toBe(enc2); + }); + + test("decrypting with wrong key throws", async () => { + const encrypted = await encrypt("secret", "correct-key"); + await expect(decrypt(encrypted, "wrong-key")).rejects.toThrow(); + }); + + test("decrypting corrupted ciphertext throws", async () => { + await expect(decrypt("not-valid-ciphertext-at-all!!", "key")).rejects.toThrow(); + }); + + test("round-trip with JSON content", async () => { + const key = "json-key"; + const message = JSON.stringify({ license_key: "abc-123", secret: true }); + const encrypted = await encrypt(message, key); + const decrypted = await decrypt(encrypted, key); + expect(JSON.parse(decrypted)).toEqual({ + license_key: "abc-123", + secret: true, + }); + }); +}); diff --git a/cli/test/lock_cache.test.ts b/cli/test/lock_cache.test.ts index c217749c6c..db1632bb27 100644 --- a/cli/test/lock_cache.test.ts +++ b/cli/test/lock_cache.test.ts @@ -10,11 +10,8 @@ * vs new logic (caches by key, skips duplicate fetches). */ -import { - assertEquals, - assertNotEquals, -} from "https://deno.land/std@0.224.0/assert/mod.ts"; -import { encodeHex } from "https://deno.land/std@0.224.0/encoding/hex.ts"; +import { expect, test } from "bun:test"; +import { encodeHex } from "@std/encoding"; // --------------------------------------------------------------------------- // Mirrors extractWorkspaceDepsAnnotation + computeLockCacheKey from @@ -163,7 +160,7 @@ async function fetchScriptLockNew( // Part 1 — Annotation parsing // ============================================================================= -Deno.test("python: manual requirements with external refs + inline deps", () => { +test("python: manual requirements with external refs + inline deps", () => { const code = `# requirements: default, base #requests==2.31.0 #pandas>=1.5.0 @@ -171,40 +168,40 @@ Deno.test("python: manual requirements with external refs + inline deps", () => def main(): pass`; const r = extractWorkspaceDepsAnnotation(code, "python3")!; - assertEquals(r.mode, "manual"); - assertEquals(r.external, ["default", "base"]); - assertEquals(r.inline, "requests==2.31.0\npandas>=1.5.0"); + expect(r.mode).toEqual("manual"); + expect(r.external).toEqual(["default", "base"]); + expect(r.inline).toEqual("requests==2.31.0\npandas>=1.5.0"); }); -Deno.test("python: extra_requirements mode", () => { +test("python: extra_requirements mode", () => { const code = `# extra_requirements: utils #numpy>=1.24.0 def main(): pass`; const r = extractWorkspaceDepsAnnotation(code, "python3")!; - assertEquals(r.mode, "extra"); - assertEquals(r.external, ["utils"]); - assertEquals(r.inline, "numpy>=1.24.0"); + expect(r.mode).toEqual("extra"); + expect(r.external).toEqual(["utils"]); + expect(r.inline).toEqual("numpy>=1.24.0"); }); -Deno.test("python: empty requirements (opt-out)", () => { +test("python: empty requirements (opt-out)", () => { const code = `# requirements: def main(): pass`; const r = extractWorkspaceDepsAnnotation(code, "python3")!; - assertEquals(r.mode, "manual"); - assertEquals(r.external, []); - assertEquals(r.inline, null); + expect(r.mode).toEqual("manual"); + expect(r.external).toEqual([]); + expect(r.inline).toEqual(null); }); -Deno.test("python: no annotation → null", () => { +test("python: no annotation → null", () => { const code = `def main(): print("hello")`; - assertEquals(extractWorkspaceDepsAnnotation(code, "python3"), null); + expect(extractWorkspaceDepsAnnotation(code, "python3")).toEqual(null); }); -Deno.test("bun: package_json annotation with inline", () => { +test("bun: package_json annotation with inline", () => { const code = `// package_json: utils, base //{ // "dependencies": { @@ -214,47 +211,47 @@ Deno.test("bun: package_json annotation with inline", () => { export function main() {}`; const r = extractWorkspaceDepsAnnotation(code, "bun")!; - assertEquals(r.mode, "manual"); - assertEquals(r.external, ["utils", "base"]); - assertEquals(r.inline, `{ + expect(r.mode).toEqual("manual"); + expect(r.external).toEqual(["utils", "base"]); + expect(r.inline).toEqual(`{ "dependencies": { "axios": "^1.6.0" } }`); }); -Deno.test("go: go_mod annotation", () => { +test("go: go_mod annotation", () => { const code = `// go_mod: base, //github.com/gin-gonic/gin v1.9.1 package main func main() {}`; const r = extractWorkspaceDepsAnnotation(code, "go")!; - assertEquals(r.mode, "manual"); - assertEquals(r.external, ["base"]); - assertEquals(r.inline, "github.com/gin-gonic/gin v1.9.1"); + expect(r.mode).toEqual("manual"); + expect(r.external).toEqual(["base"]); + expect(r.inline).toEqual("github.com/gin-gonic/gin v1.9.1"); }); -Deno.test("unsupported language → null", () => { - assertEquals(extractWorkspaceDepsAnnotation("print(1)", "deno"), null); - assertEquals(extractWorkspaceDepsAnnotation("print(1)", "bash"), null); +test("unsupported language → null", () => { + expect(extractWorkspaceDepsAnnotation("print(1)", "deno")).toEqual(null); + expect(extractWorkspaceDepsAnnotation("print(1)", "bash")).toEqual(null); }); // ============================================================================= // Part 2 — Cache key computation // ============================================================================= -Deno.test("same annotation + language + deps → same key", async () => { +test("same annotation + language + deps → same key", async () => { const code = `# requirements: default #requests==2.31.0 print("hello")`; const deps = { "dependencies/requirements.in": "requests==2.31.0" }; const a = await computeLockCacheKey(code, "python3", deps); const b = await computeLockCacheKey(code, "python3", deps); - assertEquals(a, b); + expect(a).toEqual(b); }); -Deno.test("different code, same annotation → same key", async () => { +test("different code, same annotation → same key", async () => { const codeA = `# requirements: default #requests==2.31.0 print("hello")`; @@ -262,13 +259,10 @@ print("hello")`; #requests==2.31.0 print("world")`; const deps = { "dependencies/requirements.in": "requests==2.31.0" }; - assertEquals( - await computeLockCacheKey(codeA, "python3", deps), - await computeLockCacheKey(codeB, "python3", deps), - ); + expect(await computeLockCacheKey(codeA, "python3", deps)).toEqual(await computeLockCacheKey(codeB, "python3", deps)); }); -Deno.test("different annotation inline → different key", async () => { +test("different annotation inline → different key", async () => { const codeA = `# requirements: default #requests==2.31.0 print("hello")`; @@ -276,67 +270,46 @@ print("hello")`; #flask==3.0.0 print("hello")`; const deps = { "dependencies/requirements.in": "requests==2.31.0" }; - assertNotEquals( - await computeLockCacheKey(codeA, "python3", deps), - await computeLockCacheKey(codeB, "python3", deps), - ); + expect(await computeLockCacheKey(codeA, "python3", deps)).not.toEqual(await computeLockCacheKey(codeB, "python3", deps)); }); -Deno.test("different annotation external refs → different key", async () => { +test("different annotation external refs → different key", async () => { const codeA = `# requirements: default print("hello")`; const codeB = `# requirements: base print("hello")`; const deps = { "dependencies/requirements.in": "requests==2.31.0" }; - assertNotEquals( - await computeLockCacheKey(codeA, "python3", deps), - await computeLockCacheKey(codeB, "python3", deps), - ); + expect(await computeLockCacheKey(codeA, "python3", deps)).not.toEqual(await computeLockCacheKey(codeB, "python3", deps)); }); -Deno.test("manual vs extra mode → different key", async () => { +test("manual vs extra mode → different key", async () => { const codeA = `# requirements: default print("hello")`; const codeB = `# extra_requirements: default print("hello")`; const deps = { "dependencies/requirements.in": "requests==2.31.0" }; - assertNotEquals( - await computeLockCacheKey(codeA, "python3", deps), - await computeLockCacheKey(codeB, "python3", deps), - ); + expect(await computeLockCacheKey(codeA, "python3", deps)).not.toEqual(await computeLockCacheKey(codeB, "python3", deps)); }); -Deno.test("no annotation, same code → same key", async () => { +test("no annotation, same code → same key", async () => { const deps = { "dependencies/requirements.in": "requests==2.31.0" }; - assertEquals( - await computeLockCacheKey("print('a')", "python3", deps), - await computeLockCacheKey("print('b')", "python3", deps), - ); + expect(await computeLockCacheKey("print('a')", "python3", deps)).toEqual(await computeLockCacheKey("print('b')", "python3", deps)); }); -Deno.test("different deps → different key", async () => { +test("different deps → different key", async () => { const code = `# requirements: default print("hello")`; - assertNotEquals( - await computeLockCacheKey(code, "python3", { d: "a" }), - await computeLockCacheKey(code, "python3", { d: "b" }), - ); + expect(await computeLockCacheKey(code, "python3", { d: "a" })).not.toEqual(await computeLockCacheKey(code, "python3", { d: "b" })); }); -Deno.test("different language → different key", async () => { +test("different language → different key", async () => { const deps = { d: "v" }; - assertNotEquals( - await computeLockCacheKey("x", "bun", deps), - await computeLockCacheKey("x", "python3", deps), - ); + expect(await computeLockCacheKey("x", "bun", deps)).not.toEqual(await computeLockCacheKey("x", "python3", deps)); }); -Deno.test("dep key order does not matter", async () => { +test("dep key order does not matter", async () => { const code = "print('hello')"; - assertEquals( - await computeLockCacheKey(code, "python3", { a: "1", b: "2" }), - await computeLockCacheKey(code, "python3", { b: "2", a: "1" }), - ); + expect(await computeLockCacheKey(code, "python3", { a: "1", b: "2" })).toEqual(await computeLockCacheKey(code, "python3", { b: "2", a: "1" })); }); // ============================================================================= @@ -360,7 +333,7 @@ function makeRemoteFn(): { // -- Two scripts, same annotation + language + deps ------------------------- -Deno.test("old logic: two scripts same annotation → 2 remote calls", async () => { +test("old logic: two scripts same annotation → 2 remote calls", async () => { const { remoteFn, callCount } = makeRemoteFn(); const deps = { "dependencies/requirements.in": "requests==2.31.0" }; @@ -370,10 +343,10 @@ Deno.test("old logic: two scripts same annotation → 2 remote calls", async () ]; for (const s of scripts) await fetchScriptLockOld(s, remoteFn); - assertEquals(callCount(), 2); + expect(callCount()).toEqual(2); }); -Deno.test("new logic: two scripts same annotation → 1 remote call (cache shared)", async () => { +test("new logic: two scripts same annotation → 1 remote call (cache shared)", async () => { const { remoteFn, callCount } = makeRemoteFn(); const cache = new Map(); const deps = { "dependencies/requirements.in": "requests==2.31.0" }; @@ -385,13 +358,13 @@ Deno.test("new logic: two scripts same annotation → 1 remote call (cache share const results: string[] = []; for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache)); - assertEquals(callCount(), 1); - assertEquals(results[0], results[1]); + expect(callCount()).toEqual(1); + expect(results[0]).toEqual(results[1]); }); // -- Two scripts, different annotations + same deps ------------------------- -Deno.test("new logic: different annotations same deps → 2 remote calls", async () => { +test("new logic: different annotations same deps → 2 remote calls", async () => { const { remoteFn, callCount } = makeRemoteFn(); const cache = new Map(); const deps = { "dependencies/requirements.in": "requests==2.31.0" }; @@ -403,13 +376,13 @@ Deno.test("new logic: different annotations same deps → 2 remote calls", async const results: string[] = []; for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache)); - assertEquals(callCount(), 2); - assertNotEquals(results[0], results[1]); + expect(callCount()).toEqual(2); + expect(results[0]).not.toEqual(results[1]); }); // -- Two scripts, same annotation + different deps -------------------------- -Deno.test("new logic: same annotation different deps → 2 remote calls", async () => { +test("new logic: same annotation different deps → 2 remote calls", async () => { const { remoteFn, callCount } = makeRemoteFn(); const cache = new Map(); @@ -422,13 +395,13 @@ Deno.test("new logic: same annotation different deps → 2 remote calls", async const results: string[] = []; for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache)); - assertEquals(callCount(), 2); - assertNotEquals(results[0], results[1]); + expect(callCount()).toEqual(2); + expect(results[0]).not.toEqual(results[1]); }); // -- Many scripts, same annotation + deps ----------------------------------- -Deno.test("old logic: 5 scripts same annotation+deps → 5 remote calls", async () => { +test("old logic: 5 scripts same annotation+deps → 5 remote calls", async () => { const { remoteFn, callCount } = makeRemoteFn(); const deps = { "dependencies/requirements.in": "requests==2.31.0" }; const ann = "# requirements: default\n"; @@ -442,10 +415,10 @@ Deno.test("old logic: 5 scripts same annotation+deps → 5 remote calls", async ]; for (const s of scripts) await fetchScriptLockOld(s, remoteFn); - assertEquals(callCount(), 5); + expect(callCount()).toEqual(5); }); -Deno.test("new logic: 5 scripts same annotation+deps → 1 remote call", async () => { +test("new logic: 5 scripts same annotation+deps → 1 remote call", async () => { const { remoteFn, callCount } = makeRemoteFn(); const cache = new Map(); const deps = { "dependencies/requirements.in": "requests==2.31.0" }; @@ -461,15 +434,15 @@ Deno.test("new logic: 5 scripts same annotation+deps → 1 remote call", async ( const results: string[] = []; for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache)); - assertEquals(callCount(), 1); + expect(callCount()).toEqual(1); for (let i = 1; i < results.length; i++) { - assertEquals(results[0], results[i]); + expect(results[0]).toEqual(results[i]); } }); // -- Many scripts, 2 annotation groups + same deps ------------------------- -Deno.test("new logic: 4 scripts with 2 annotation groups → 2 remote calls", async () => { +test("new logic: 4 scripts with 2 annotation groups → 2 remote calls", async () => { const { remoteFn, callCount } = makeRemoteFn(); const cache = new Map(); const deps = { "dependencies/requirements.in": "requests==2.31.0" }; @@ -483,15 +456,15 @@ Deno.test("new logic: 4 scripts with 2 annotation groups → 2 remote calls", as const results: string[] = []; for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache)); - assertEquals(callCount(), 2); - assertEquals(results[0], results[2]); // same annotation "default" - assertEquals(results[1], results[3]); // same annotation "base" - assertNotEquals(results[0], results[1]); + expect(callCount()).toEqual(2); + expect(results[0]).toEqual(results[2]); // same annotation "default" + expect(results[1]).toEqual(results[3]); // same annotation "base" + expect(results[0]).not.toEqual(results[1]); }); // -- Scripts with no workspace deps (empty) --------------------------------- -Deno.test("new logic: empty deps → no caching", async () => { +test("new logic: empty deps → no caching", async () => { const { remoteFn, callCount } = makeRemoteFn(); const cache = new Map(); @@ -501,13 +474,13 @@ Deno.test("new logic: empty deps → no caching", async () => { ]; for (const s of scripts) await fetchScriptLockNew(s, remoteFn, cache); - assertEquals(callCount(), 2); - assertEquals(cache.size, 0); + expect(callCount()).toEqual(2); + expect(cache.size).toEqual(0); }); // -- No annotation scripts with raw deps → share cache --------------------- -Deno.test("new logic: no annotation + same deps → 1 remote call", async () => { +test("new logic: no annotation + same deps → 1 remote call", async () => { const { remoteFn, callCount } = makeRemoteFn(); const cache = new Map(); const deps = { "dependencies/requirements.in": "requests==2.31.0" }; @@ -519,13 +492,13 @@ Deno.test("new logic: no annotation + same deps → 1 remote call", async () => const results: string[] = []; for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache)); - assertEquals(callCount(), 1); - assertEquals(results[0], results[1]); + expect(callCount()).toEqual(1); + expect(results[0]).toEqual(results[1]); }); // -- Mix of annotated and non-annotated scripts ----------------------------- -Deno.test("new logic: mix of annotated and non-annotated → separate cache groups", async () => { +test("new logic: mix of annotated and non-annotated → separate cache groups", async () => { const { remoteFn, callCount } = makeRemoteFn(); const cache = new Map(); const deps = { "dependencies/requirements.in": "requests==2.31.0" }; @@ -539,15 +512,15 @@ Deno.test("new logic: mix of annotated and non-annotated → separate cache grou const results: string[] = []; for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache)); - assertEquals(callCount(), 2); // one for annotated group, one for no-annotation group - assertEquals(results[0], results[2]); // both annotated "default" - assertEquals(results[1], results[3]); // both no annotation - assertNotEquals(results[0], results[1]); // annotated ≠ non-annotated + expect(callCount()).toEqual(2); // one for annotated group, one for no-annotation group + expect(results[0]).toEqual(results[2]); // both annotated "default" + expect(results[1]).toEqual(results[3]); // both no annotation + expect(results[0]).not.toEqual(results[1]); // annotated ≠ non-annotated }); // -- Cache returns correct lock value --------------------------------------- -Deno.test("new logic: cached value matches original remote response", async () => { +test("new logic: cached value matches original remote response", async () => { const cache = new Map(); const deps = { "dependencies/requirements.in": "requests==2.31.0" }; @@ -566,7 +539,7 @@ Deno.test("new logic: cached value matches original remote response", async () = remoteFn, cache, ); - assertEquals(callIdx, 1); - assertEquals(r1, "resolved-lock-content-abc123"); - assertEquals(r2, "resolved-lock-content-abc123"); + expect(callIdx).toEqual(1); + expect(r1).toEqual("resolved-lock-content-abc123"); + expect(r2).toEqual("resolved-lock-content-abc123"); }); diff --git a/cli/test/locks_required.test.ts b/cli/test/locks_required.test.ts deleted file mode 100644 index 150c24593c..0000000000 --- a/cli/test/locks_required.test.ts +++ /dev/null @@ -1,620 +0,0 @@ -import { - assert, - assertEquals, -} from "https://deno.land/std@0.224.0/assert/mod.ts"; -import { - checkMissingLocks, - runLint, -} from "../src/commands/lint/lint.ts"; - -async function withTempDir( - fn: (tempDir: string) => Promise, -): Promise { - const tempDir = await Deno.makeTempDir({ prefix: "wmill_locks_test_" }); - const originalCwd = Deno.cwd(); - try { - Deno.chdir(tempDir); - await fn(tempDir); - } finally { - Deno.chdir(originalCwd); - await Deno.remove(tempDir, { recursive: true }); - } -} - -// --- checkMissingLocks unit tests --- - -Deno.test("locks-required: passes for python script with non-empty lock file", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.py`, - `import pandas\ndef main(): pass`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.script.yaml`, - `summary: ""\ndescription: ""\nlock: "!inline f/folder/my_script.script.lock"\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.script.lock`, - `pandas==2.0.0\nnumpy==1.24.0\n`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 0); - }); -}); - -Deno.test("locks-required: fails for python script with empty lock file", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.py`, - `def main(): pass`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.script.yaml`, - `summary: ""\ndescription: ""\nlock: "!inline f/folder/my_script.script.lock"\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.script.lock`, - ``, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 1); - assertEquals(issues[0].target, "script"); - assert(issues[0].errors[0].includes("Missing lock")); - assert(issues[0].errors[0].includes("python3")); - }); -}); - -Deno.test("locks-required: fails for python script with missing lock file", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.py`, - `def main(): pass`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.script.yaml`, - `summary: ""\ndescription: ""\nlock: "!inline f/folder/nonexistent.script.lock"\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 1); - assertEquals(issues[0].target, "script"); - }); -}); - -Deno.test("locks-required: fails for python script with lock field empty string", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.py`, - `def main(): pass`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.script.yaml`, - `summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 1); - assertEquals(issues[0].target, "script"); - }); -}); - -Deno.test("locks-required: skips bash scripts (no locks needed)", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.sh`, - `#!/bin/bash\necho hello`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.script.yaml`, - `summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 0); - }); -}); - -Deno.test("locks-required: skips SQL scripts (no locks needed)", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/folder/my_query.pg.sql`, - `SELECT 1;`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_query.script.yaml`, - `summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 0); - }); -}); - -Deno.test("locks-required: checks bun typescript scripts", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.bun.ts`, - `export async function main() { return "hello"; }`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.script.yaml`, - `summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 1); - assert(issues[0].errors[0].includes("bun")); - }); -}); - -Deno.test("locks-required: checks deno typescript scripts", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.deno.ts`, - `export async function main() { return "hello"; }`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.script.yaml`, - `summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 1); - assert(issues[0].errors[0].includes("deno")); - }); -}); - -// --- Flow inline script tests --- - -Deno.test("locks-required: fails for flow with unlocked inline python script", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_flow.flow/inline_script_0.inline_script.py`, - `def main(): pass`, - ); - await Deno.writeTextFile( - `${tempDir}/f/my_flow.flow/flow.yaml`, - `summary: My flow -value: - modules: - - id: a - value: - type: rawscript - language: python3 - content: "!inline inline_script_0.inline_script.py" - lock: "" -`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 1); - assertEquals(issues[0].target, "flow_inline_script"); - assert(issues[0].errors[0].includes("python3")); - assert(issues[0].errors[0].includes("'a'")); - }); -}); - -Deno.test("locks-required: passes for flow with locked inline python script", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_flow.flow/inline_script_0.inline_script.py`, - `import pandas\ndef main(): pass`, - ); - await Deno.writeTextFile( - `${tempDir}/f/my_flow.flow/inline_script_0.inline_script.lock`, - `pandas==2.0.0\n`, - ); - await Deno.writeTextFile( - `${tempDir}/f/my_flow.flow/flow.yaml`, - `summary: My flow -value: - modules: - - id: a - value: - type: rawscript - language: python3 - content: "!inline inline_script_0.inline_script.py" - lock: "!inline inline_script_0.inline_script.lock" -`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 0); - }); -}); - -Deno.test("locks-required: skips flow inline bash scripts", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_flow.flow/flow.yaml`, - `summary: My flow -value: - modules: - - id: a - value: - type: rawscript - language: bash - content: "echo hello" - lock: "" -`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 0); - }); -}); - -Deno.test("locks-required: checks nested flow modules (forloopflow)", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_flow.flow/flow.yaml`, - `summary: My flow -value: - modules: - - id: loop - value: - type: forloopflow - modules: - - id: inner - value: - type: rawscript - language: python3 - content: "def main(): pass" -`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 1); - assert(issues[0].errors[0].includes("'inner'")); - }); -}); - -Deno.test("locks-required: checks nested flow modules (branchone)", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_flow.flow/flow.yaml`, - `summary: My flow -value: - modules: - - id: branch - value: - type: branchone - branches: - - modules: - - id: branch_script - value: - type: rawscript - language: bun - content: "export async function main() {}" - default: - - id: default_script - value: - type: rawscript - language: python3 - content: "def main(): pass" -`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 2); - const ids = issues.map((i) => i.errors[0]); - assert(ids.some((e) => e.includes("'branch_script'"))); - assert(ids.some((e) => e.includes("'default_script'"))); - }); -}); - -// --- Integration with runLint --- - -Deno.test("locks-required: runLint includes lock issues when flag is set", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.py`, - `def main(): pass`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.script.yaml`, - `summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - - const report = await runLint({ locksRequired: true } as any, tempDir); - assertEquals(report.success, false); - assertEquals(report.exitCode, 1); - assert(report.issues.some((i) => i.target === "script")); - }); -}); - -Deno.test("locks-required: runLint skips lock check when flag is not set", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.py`, - `def main(): pass`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/my_script.script.yaml`, - `summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - - const report = await runLint({} as any, tempDir); - assertEquals(report.success, true); - assertEquals(report.exitCode, 0); - assertEquals(report.issues.length, 0); - }); -}); - -// --- Multiple scripts --- - -Deno.test("locks-required: reports multiple missing locks", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true }); - - // Python script without lock - await Deno.writeTextFile( - `${tempDir}/f/folder/script1.py`, - `def main(): pass`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/script1.script.yaml`, - `summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - - // Go script without lock - await Deno.writeTextFile( - `${tempDir}/f/folder/script2.go`, - `package main\nfunc main() {}`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/script2.script.yaml`, - `summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - - // Bash script (should pass - no lock needed) - await Deno.writeTextFile( - `${tempDir}/f/folder/script3.sh`, - `#!/bin/bash\necho ok`, - ); - await Deno.writeTextFile( - `${tempDir}/f/folder/script3.script.yaml`, - `summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 2); - assert(issues.every((i) => i.target === "script")); - }); -}); - -// --- Normal app inline script tests --- - -Deno.test("locks-required: fails for app with unlocked inline python script", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_app.app`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_app.app/app.yaml`, - `summary: My app -value: - grid: - - data: - inlineScript: - content: "def main(): pass" - language: python3 - lock: "" -`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 1); - assertEquals(issues[0].target, "app_inline_script"); - assert(issues[0].errors[0].includes("python3")); - }); -}); - -Deno.test("locks-required: passes for app with locked inline python script", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_app.app`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_app.app/inline_script_0.inline_script.lock`, - `pandas==2.0.0\n`, - ); - await Deno.writeTextFile( - `${tempDir}/f/my_app.app/app.yaml`, - `summary: My app -value: - grid: - - data: - inlineScript: - content: "import pandas" - language: python3 - lock: "!inline inline_script_0.inline_script.lock" -`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 0); - }); -}); - -Deno.test("locks-required: skips app inline bash scripts", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_app.app`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_app.app/app.yaml`, - `summary: My app -value: - grid: - - data: - inlineScript: - content: "echo hello" - language: bash - lock: "" -`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 0); - }); -}); - -Deno.test("locks-required: finds deeply nested app inline scripts", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_app.app`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_app.app/app.yaml`, - `summary: My app -value: - grid: - - components: - - nested: - deeper: - inlineScript: - content: "export async function main() {}" - language: bun - lock: "" -`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 1); - assertEquals(issues[0].target, "app_inline_script"); - assert(issues[0].errors[0].includes("bun")); - }); -}); - -// --- Raw app backend script tests --- - -Deno.test("locks-required: fails for raw app with unlocked backend python script", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_app.raw_app/backend`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/raw_app.yaml`, - `summary: My raw app -`, - ); - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/backend/get_data.yaml`, - `type: inline -`, - ); - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/backend/get_data.py`, - `def main(): pass`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 1); - assertEquals(issues[0].target, "raw_app_inline_script"); - assert(issues[0].errors[0].includes("python3")); - assert(issues[0].errors[0].includes("get_data")); - }); -}); - -Deno.test("locks-required: passes for raw app with locked backend python script", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_app.raw_app/backend`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/raw_app.yaml`, - `summary: My raw app -`, - ); - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/backend/get_data.yaml`, - `type: inline -`, - ); - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/backend/get_data.py`, - `import pandas\ndef main(): pass`, - ); - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/backend/get_data.lock`, - `pandas==2.0.0\n`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 0); - }); -}); - -Deno.test("locks-required: raw app auto-detects code files without YAML config", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_app.raw_app/backend`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/raw_app.yaml`, - `summary: My raw app -`, - ); - // No .yaml config, just a code file - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/backend/fetch_users.bun.ts`, - `export async function main() { return []; }`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 1); - assertEquals(issues[0].target, "raw_app_inline_script"); - assert(issues[0].errors[0].includes("bun")); - assert(issues[0].errors[0].includes("fetch_users")); - }); -}); - -Deno.test("locks-required: skips raw app bash backend scripts", async () => { - await withTempDir(async (tempDir) => { - await Deno.mkdir(`${tempDir}/f/my_app.raw_app/backend`, { recursive: true }); - - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/raw_app.yaml`, - `summary: My raw app -`, - ); - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/backend/cleanup.yaml`, - `type: inline -`, - ); - await Deno.writeTextFile( - `${tempDir}/f/my_app.raw_app/backend/cleanup.sh`, - `#!/bin/bash\necho done`, - ); - - const issues = await checkMissingLocks({} as any, tempDir); - assertEquals(issues.length, 0); - }); -}); diff --git a/cli/test/mixed_case_paths.test.ts b/cli/test/mixed_case_paths.test.ts index 74c7674678..d0518cb511 100644 --- a/cli/test/mixed_case_paths.test.ts +++ b/cli/test/mixed_case_paths.test.ts @@ -12,9 +12,9 @@ * 3. The modifications are correctly applied on the server */ -import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; -import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts"; -import * as path from "https://deno.land/std@0.224.0/path/mod.ts"; +import { expect, test } from "bun:test"; +import * as path from "@std/path"; +import { writeFile, readFile, stat } from "node:fs/promises"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; import { parseJsonFromCLIOutput } from "./test_config_helpers.ts"; @@ -250,27 +250,19 @@ async function verifyNoDiffOnPull(backend: any, tempDir: string): Promise ["sync", "pull", "--yes", "--dry-run", "--json-output"], tempDir ); - assertEquals(pullResult.code, 0, `Pull for diff check should succeed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); const output = parseJsonFromCLIOutput(pullResult.stdout); const changes = output.changes || []; - assertEquals( - changes.length, - 0, - `Should have no changes after push, but found: ${JSON.stringify(changes.map((c: any) => c.path))}` - ); + expect(changes.length).toEqual(0); } // ============================================================================= // TESTS // ============================================================================= -Deno.test({ - name: "Mixed Case Paths: pull and push script with capitalized folder", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Mixed Case Paths: pull and push script with capitalized folder", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); @@ -285,56 +277,51 @@ Deno.test({ await createScript(backend, scriptPath, originalContent, "My Test Script"); // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] -` +`, + "utf-8" ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Verify file exists with correct path (normalized for comparison) const expectedScriptPath = path.join(tempDir, "f", "MyFolder", "MyScript.ts"); - const scriptExists = await Deno.stat(expectedScriptPath).then(() => true).catch(() => false); - assert(scriptExists, `Script file should exist at ${expectedScriptPath}`); + const scriptExists = await stat(expectedScriptPath).then(() => true).catch(() => false); + expect(scriptExists).toBeTruthy(); // Read and verify content - const pulledContent = await Deno.readTextFile(expectedScriptPath); - assert(pulledContent.includes("original content"), "Pulled content should match original"); + const pulledContent = await readFile(expectedScriptPath, "utf-8"); + expect(pulledContent.includes("original content")).toBeTruthy(); // Modify the script const modifiedContent = `export async function main() { return "modified content from test"; }`; - await Deno.writeTextFile(expectedScriptPath, modifiedContent); + await writeFile(expectedScriptPath, modifiedContent, "utf-8"); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); - assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); + expect(pushResult.code).toEqual(0); // Verify modification on server const updatedScript = await getScript(backend, scriptPath); - assert( - updatedScript.content.includes("modified content from test"), - `Server should have modified content. Got: ${updatedScript.content}` - ); + expect( + updatedScript.content.includes("modified content from test") + ).toBeTruthy(); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); - }, }); -Deno.test({ - name: "Mixed Case Paths: pull and push flow with capitalized folder", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Mixed Case Paths: pull and push flow with capitalized folder", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); @@ -349,59 +336,51 @@ Deno.test({ await createFlow(backend, flowPath, originalContent, "Data Processor Flow"); // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] -` +`, + "utf-8" ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Verify flow directory exists const flowDir = path.join(tempDir, "f", "MyFlows", "DataProcessor.flow"); - const flowDirExists = await Deno.stat(flowDir).then(s => s.isDirectory).catch(() => false); - assert(flowDirExists, `Flow directory should exist at ${flowDir}`); + const flowDirExists = await stat(flowDir).then(s => s.isDirectory()).catch(() => false); + expect(flowDirExists).toBeTruthy(); // Modify the flow metadata (summary) instead of inline script const flowMetadataPath = path.join(flowDir, "flow.yaml"); - const flowMetadataExists = await Deno.stat(flowMetadataPath).then(() => true).catch(() => false); - assert(flowMetadataExists, `Flow metadata should exist at ${flowMetadataPath}`); + const flowMetadataExists = await stat(flowMetadataPath).then(() => true).catch(() => false); + expect(flowMetadataExists).toBeTruthy(); - const flowMetadata = await Deno.readTextFile(flowMetadataPath); + const flowMetadata = await readFile(flowMetadataPath, "utf-8"); const modifiedMetadata = flowMetadata.replace( /summary:.*$/m, 'summary: "Modified Data Processor Flow from test"' ); - await Deno.writeTextFile(flowMetadataPath, modifiedMetadata); + await writeFile(flowMetadataPath, modifiedMetadata, "utf-8"); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); - assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); + expect(pushResult.code).toEqual(0); // Verify modification on server const updatedFlow = await getFlow(backend, flowPath); - assertEquals( - updatedFlow.summary, - "Modified Data Processor Flow from test", - `Server should have modified flow summary. Got: ${updatedFlow.summary}` - ); + expect(updatedFlow.summary).toEqual("Modified Data Processor Flow from test"); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); - }, }); -Deno.test({ - name: "Mixed Case Paths: pull and push app with capitalized folder", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Mixed Case Paths: pull and push app with capitalized folder", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); @@ -413,56 +392,48 @@ Deno.test({ await createApp(backend, appPath, "My Dashboard App"); // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] -` +`, + "utf-8" ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Verify app directory exists const appDir = path.join(tempDir, "f", "MyApps", "Dashboard.app"); - const appDirExists = await Deno.stat(appDir).then(s => s.isDirectory).catch(() => false); - assert(appDirExists, `App directory should exist at ${appDir}`); + const appDirExists = await stat(appDir).then(s => s.isDirectory()).catch(() => false); + expect(appDirExists).toBeTruthy(); // Modify the app metadata const appMetadataPath = path.join(appDir, "app.yaml"); - const appMetadata = await Deno.readTextFile(appMetadataPath); + const appMetadata = await readFile(appMetadataPath, "utf-8"); const modifiedMetadata = appMetadata.replace( /summary:.*$/m, 'summary: "Modified Dashboard App from test"' ); - await Deno.writeTextFile(appMetadataPath, modifiedMetadata); + await writeFile(appMetadataPath, modifiedMetadata, "utf-8"); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); - assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); + expect(pushResult.code).toEqual(0); // Verify modification on server const updatedApp = await getApp(backend, appPath); - assertEquals( - updatedApp.summary, - "Modified Dashboard App from test", - `Server should have modified app summary. Got: ${updatedApp.summary}` - ); + expect(updatedApp.summary).toEqual("Modified Dashboard App from test"); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); - }, }); -Deno.test({ - name: "Mixed Case Paths: pull and push variable with capitalized folder", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Mixed Case Paths: pull and push variable with capitalized folder", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); @@ -474,55 +445,47 @@ Deno.test({ await createVariable(backend, varPath, "original-api-key-value", "API Key Variable"); // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] -` +`, + "utf-8" ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Verify variable file exists const varFilePath = path.join(tempDir, "f", "MyVars", "ApiKey.variable.yaml"); - const varExists = await Deno.stat(varFilePath).then(() => true).catch(() => false); - assert(varExists, `Variable file should exist at ${varFilePath}`); + const varExists = await stat(varFilePath).then(() => true).catch(() => false); + expect(varExists).toBeTruthy(); // Modify the variable - const varContent = await Deno.readTextFile(varFilePath); + const varContent = await readFile(varFilePath, "utf-8"); const modifiedVarContent = varContent.replace( /value:.*$/m, 'value: "modified-api-key-from-test"' ); - await Deno.writeTextFile(varFilePath, modifiedVarContent); + await writeFile(varFilePath, modifiedVarContent, "utf-8"); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); - assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); + expect(pushResult.code).toEqual(0); // Verify modification on server const updatedVar = await getVariable(backend, varPath); - assertEquals( - updatedVar.value, - "modified-api-key-from-test", - `Server should have modified variable value. Got: ${updatedVar.value}` - ); + expect(updatedVar.value).toEqual("modified-api-key-from-test"); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); - }, }); -Deno.test({ - name: "Mixed Case Paths: deeply nested capitalized folders", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Mixed Case Paths: deeply nested capitalized folders", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); @@ -539,52 +502,47 @@ Deno.test({ await createScript(backend, scriptPath, originalContent, "Nested Script"); // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] -` +`, + "utf-8" ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Verify file exists const scriptFilePath = path.join(tempDir, "f", "MyProject", "SubFolder_A.ts"); - const scriptExists = await Deno.stat(scriptFilePath).then(() => true).catch(() => false); - assert(scriptExists, `Nested script should exist at ${scriptFilePath}`); + const scriptExists = await stat(scriptFilePath).then(() => true).catch(() => false); + expect(scriptExists).toBeTruthy(); // Modify const modifiedContent = `export async function main() { return "deeply nested modified from test"; }`; - await Deno.writeTextFile(scriptFilePath, modifiedContent); + await writeFile(scriptFilePath, modifiedContent, "utf-8"); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); - assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); + expect(pushResult.code).toEqual(0); // Verify on server const updatedScript = await getScript(backend, scriptPath); - assert( - updatedScript.content.includes("deeply nested modified from test"), - `Server should have modified nested content` - ); + expect( + updatedScript.content.includes("deeply nested modified from test") + ).toBeTruthy(); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); - }, }); -Deno.test({ - name: "Mixed Case Paths: multiple resources in same capitalized folder", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Mixed Case Paths: multiple resources in same capitalized folder", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); @@ -608,65 +566,63 @@ Deno.test({ await createResource(backend, "f/SharedFolder/ResourceOne", "any", { key: "original" }); // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] -` +`, + "utf-8" ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Verify all files exist const folderPath = path.join(tempDir, "f", "SharedFolder"); - const script1Exists = await Deno.stat(path.join(folderPath, "ScriptOne.ts")).then(() => true).catch(() => false); - const script2Exists = await Deno.stat(path.join(folderPath, "ScriptTwo.ts")).then(() => true).catch(() => false); - const var1Exists = await Deno.stat(path.join(folderPath, "VarOne.variable.yaml")).then(() => true).catch(() => false); - const res1Exists = await Deno.stat(path.join(folderPath, "ResourceOne.resource.yaml")).then(() => true).catch(() => false); + const script1Exists = await stat(path.join(folderPath, "ScriptOne.ts")).then(() => true).catch(() => false); + const script2Exists = await stat(path.join(folderPath, "ScriptTwo.ts")).then(() => true).catch(() => false); + const var1Exists = await stat(path.join(folderPath, "VarOne.variable.yaml")).then(() => true).catch(() => false); + const res1Exists = await stat(path.join(folderPath, "ResourceOne.resource.yaml")).then(() => true).catch(() => false); - assert(script1Exists, "ScriptOne should exist"); - assert(script2Exists, "ScriptTwo should exist"); - assert(var1Exists, "VarOne should exist"); - assert(res1Exists, "ResourceOne should exist"); + expect(script1Exists).toBeTruthy(); + expect(script2Exists).toBeTruthy(); + expect(var1Exists).toBeTruthy(); + expect(res1Exists).toBeTruthy(); // Modify script one - await Deno.writeTextFile( + await writeFile( path.join(folderPath, "ScriptOne.ts"), - 'export async function main() { return "script one MODIFIED"; }' + 'export async function main() { return "script one MODIFIED"; }', + "utf-8" ); // Modify script two - await Deno.writeTextFile( + await writeFile( path.join(folderPath, "ScriptTwo.ts"), - 'export async function main() { return "script two MODIFIED"; }' + 'export async function main() { return "script two MODIFIED"; }', + "utf-8" ); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); - assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); + expect(pushResult.code).toEqual(0); // Verify modifications on server const script1 = await getScript(backend, "f/SharedFolder/ScriptOne"); const script2 = await getScript(backend, "f/SharedFolder/ScriptTwo"); - assert(script1.content.includes("script one MODIFIED"), "Script one should be modified on server"); - assert(script2.content.includes("script two MODIFIED"), "Script two should be modified on server"); + expect(script1.content.includes("script one MODIFIED")).toBeTruthy(); + expect(script2.content.includes("script two MODIFIED")).toBeTruthy(); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); - }, }); -Deno.test({ - name: "Mixed Case Paths: CamelCase folder names with numbers", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Mixed Case Paths: CamelCase folder names with numbers", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); @@ -683,40 +639,41 @@ Deno.test({ ); // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] -` +`, + "utf-8" ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Verify file exists const scriptFilePath = path.join(tempDir, "f", "Project2024", "DataHandler_V2.ts"); - const scriptExists = await Deno.stat(scriptFilePath).then(() => true).catch(() => false); - assert(scriptExists, `Script should exist at ${scriptFilePath}`); + const scriptExists = await stat(scriptFilePath).then(() => true).catch(() => false); + expect(scriptExists).toBeTruthy(); // Modify - await Deno.writeTextFile( + await writeFile( scriptFilePath, - 'export async function main() { return "handler v2 MODIFIED"; }' + 'export async function main() { return "handler v2 MODIFIED"; }', + "utf-8" ); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); - assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); + expect(pushResult.code).toEqual(0); // Verify on server const updatedScript = await getScript(backend, scriptPath); - assert(updatedScript.content.includes("handler v2 MODIFIED"), "Server should have modified content"); + expect(updatedScript.content.includes("handler v2 MODIFIED")).toBeTruthy(); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); - }, }); diff --git a/cli/test/multi_instance_workspace.test.ts b/cli/test/multi_instance_workspace.test.ts index 9cdd331477..1d0172166d 100644 --- a/cli/test/multi_instance_workspace.test.ts +++ b/cli/test/multi_instance_workspace.test.ts @@ -1,4 +1,5 @@ -import { assertEquals, assert, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; +import { writeFile } from "node:fs/promises"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; import { parseJsonFromCLIOutput } from "./test_config_helpers.ts"; @@ -20,16 +21,12 @@ async function setupWorkspaceProfile(backend: any, workspaceName: string): Promi await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); } -Deno.test({ - name: "Multi-Branch: sync pull with branch-specific overrides", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Multi-Branch: sync pull with branch-specific overrides", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend, "multi_branch_test"); // Create wmill.yaml with gitBranches configuration - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] @@ -46,7 +43,7 @@ gitBranches: prod: overrides: skipVariables: true - skipResources: true`); + skipResources: true`, "utf-8"); // Test main branch - should include variables and resources const mainResult = await backend.runCLICommand([ @@ -56,7 +53,7 @@ gitBranches: '--json-output' ], tempDir, "multi_branch_test"); - assertEquals(mainResult.code, 0, `Main branch sync should succeed: ${mainResult.stderr}`); + expect(mainResult.code).toEqual(0); const mainData = parseJsonFromCLIOutput(mainResult.stdout); const mainPaths = (mainData.changes || []).map((c: any) => c.path); @@ -64,8 +61,8 @@ gitBranches: const mainHasVariables = mainPaths.some((path: string) => path.includes('.variable.yaml')); const mainHasResources = mainPaths.some((path: string) => path.includes('.resource.yaml')); - assertEquals(mainHasVariables, true, "Main branch should include variables"); - assertEquals(mainHasResources, true, "Main branch should include resources"); + expect(mainHasVariables).toEqual(true); + expect(mainHasResources).toEqual(true); // Test staging branch - should skip variables but include resources const stagingResult = await backend.runCLICommand([ @@ -75,7 +72,7 @@ gitBranches: '--json-output' ], tempDir, "multi_branch_test"); - assertEquals(stagingResult.code, 0, `Staging branch sync should succeed: ${stagingResult.stderr}`); + expect(stagingResult.code).toEqual(0); const stagingData = parseJsonFromCLIOutput(stagingResult.stdout); const stagingPaths = (stagingData.changes || []).map((c: any) => c.path); @@ -83,8 +80,8 @@ gitBranches: const stagingHasVariables = stagingPaths.some((path: string) => path.includes('.variable.yaml')); const stagingHasResources = stagingPaths.some((path: string) => path.includes('.resource.yaml')); - assertEquals(stagingHasVariables, false, "Staging branch should skip variables"); - assertEquals(stagingHasResources, true, "Staging branch should include resources"); + expect(stagingHasVariables).toEqual(false); + expect(stagingHasResources).toEqual(true); // Test prod branch - should skip both variables and resources const prodResult = await backend.runCLICommand([ @@ -94,7 +91,7 @@ gitBranches: '--json-output' ], tempDir, "multi_branch_test"); - assertEquals(prodResult.code, 0, `Prod branch sync should succeed: ${prodResult.stderr}`); + expect(prodResult.code).toEqual(0); const prodData = parseJsonFromCLIOutput(prodResult.stdout); const prodPaths = (prodData.changes || []).map((c: any) => c.path); @@ -102,21 +99,16 @@ gitBranches: const prodHasVariables = prodPaths.some((path: string) => path.includes('.variable.yaml')); const prodHasResources = prodPaths.some((path: string) => path.includes('.resource.yaml')); - assertEquals(prodHasVariables, false, "Prod branch should skip variables"); - assertEquals(prodHasResources, false, "Prod branch should skip resources"); + expect(prodHasVariables).toEqual(false); + expect(prodHasResources).toEqual(false); }); - } }); -Deno.test({ - name: "Multi-Branch: branch override with includes filtering", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Multi-Branch: branch override with includes filtering", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend, "includes_branch_test"); - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" @@ -131,7 +123,7 @@ gitBranches: includes: - "f/**" - "users/**" - skipVariables: false`); + skipVariables: false`, "utf-8"); // Test feature branch - should skip variables and only include f/** const featureResult = await backend.runCLICommand([ @@ -141,7 +133,7 @@ gitBranches: '--json-output' ], tempDir, "includes_branch_test"); - assertEquals(featureResult.code, 0, `Feature branch sync should succeed: ${featureResult.stderr}`); + expect(featureResult.code).toEqual(0); const featureData = parseJsonFromCLIOutput(featureResult.stdout); const featurePaths = (featureData.changes || []).map((c: any) => c.path); @@ -151,8 +143,8 @@ gitBranches: const featureHasVariables = normalizedFeaturePaths.some((path: string) => path.includes('.variable.yaml')); const featureHasUsers = normalizedFeaturePaths.some((path: string) => path.startsWith('users/')); - assertEquals(featureHasVariables, false, "Feature branch should skip variables"); - assertEquals(featureHasUsers, false, "Feature branch should not include users (not in includes)"); + expect(featureHasVariables).toEqual(false); + expect(featureHasUsers).toEqual(false); // Test release branch - should include variables and users const releaseResult = await backend.runCLICommand([ @@ -163,7 +155,7 @@ gitBranches: '--json-output' ], tempDir, "includes_branch_test"); - assertEquals(releaseResult.code, 0, `Release branch sync should succeed: ${releaseResult.stderr}`); + expect(releaseResult.code).toEqual(0); const releaseData = parseJsonFromCLIOutput(releaseResult.stdout); const releasePaths = (releaseData.changes || []).map((c: any) => c.path); @@ -173,21 +165,16 @@ gitBranches: const releaseHasVariables = normalizedReleasePaths.some((path: string) => path.includes('.variable.yaml')); const releaseHasUsers = normalizedReleasePaths.some((path: string) => path.startsWith('users/')); - assertEquals(releaseHasVariables, true, "Release branch should include variables"); - assertEquals(releaseHasUsers, true, "Release branch should include users"); + expect(releaseHasVariables).toEqual(true); + expect(releaseHasUsers).toEqual(true); }); - } }); -Deno.test({ - name: "Multi-Branch: fallback to base config when branch not defined", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Multi-Branch: fallback to base config when branch not defined", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend, "fallback_test"); - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" skipVariables: true @@ -197,7 +184,7 @@ gitBranches: main: overrides: skipVariables: false - skipResources: false`); + skipResources: false`, "utf-8"); // Test undefined branch - should use base config (skip variables and resources) const undefinedResult = await backend.runCLICommand([ @@ -207,7 +194,7 @@ gitBranches: '--json-output' ], tempDir, "fallback_test"); - assertEquals(undefinedResult.code, 0, `Undefined branch sync should succeed: ${undefinedResult.stderr}`); + expect(undefinedResult.code).toEqual(0); const undefinedData = parseJsonFromCLIOutput(undefinedResult.stdout); const undefinedPaths = (undefinedData.changes || []).map((c: any) => c.path); @@ -216,8 +203,8 @@ gitBranches: const undefinedHasResources = undefinedPaths.some((path: string) => path.includes('.resource.yaml')); // Should use base config since branch is not defined - assertEquals(undefinedHasVariables, false, "Undefined branch should use base config skipVariables: true"); - assertEquals(undefinedHasResources, false, "Undefined branch should use base config skipResources: true"); + expect(undefinedHasVariables).toEqual(false); + expect(undefinedHasResources).toEqual(false); // Test defined main branch - should use branch overrides const mainResult = await backend.runCLICommand([ @@ -227,7 +214,7 @@ gitBranches: '--json-output' ], tempDir, "fallback_test"); - assertEquals(mainResult.code, 0, `Main branch sync should succeed: ${mainResult.stderr}`); + expect(mainResult.code).toEqual(0); const mainData = parseJsonFromCLIOutput(mainResult.stdout); const mainPaths = (mainData.changes || []).map((c: any) => c.path); @@ -235,21 +222,16 @@ gitBranches: const mainHasVariables = mainPaths.some((path: string) => path.includes('.variable.yaml')); const mainHasResources = mainPaths.some((path: string) => path.includes('.resource.yaml')); - assertEquals(mainHasVariables, true, "Main branch should use override skipVariables: false"); - assertEquals(mainHasResources, true, "Main branch should use override skipResources: false"); + expect(mainHasVariables).toEqual(true); + expect(mainHasResources).toEqual(true); }); - } }); -Deno.test({ - name: "Multi-Branch: branch inherits unspecified settings from base", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Multi-Branch: branch inherits unspecified settings from base", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend, "inherit_test"); - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" skipVariables: true @@ -259,7 +241,7 @@ skipApps: true gitBranches: partial: overrides: - skipVariables: false`); + skipVariables: false`, "utf-8"); // Test partial branch - should inherit skipResources and skipApps from base const result = await backend.runCLICommand([ @@ -269,7 +251,7 @@ gitBranches: '--json-output' ], tempDir, "inherit_test"); - assertEquals(result.code, 0, `Partial branch sync should succeed: ${result.stderr}`); + expect(result.code).toEqual(0); const data = parseJsonFromCLIOutput(result.stdout); const paths = (data.changes || []).map((c: any) => c.path); @@ -279,10 +261,9 @@ gitBranches: const hasApps = paths.some((path: string) => path.includes('.app/') || path.endsWith('.app.yaml')); // skipVariables is overridden to false - assertEquals(hasVariables, true, "Partial branch should include variables (override)"); + expect(hasVariables).toEqual(true); // skipResources and skipApps are inherited from base (true) - assertEquals(hasResources, false, "Partial branch should skip resources (inherited)"); - assertEquals(hasApps, false, "Partial branch should skip apps (inherited)"); + expect(hasResources).toEqual(false); + expect(hasApps).toEqual(false); }); - } }); diff --git a/cli/test/override_settings_behavior.test.ts b/cli/test/override_settings_behavior.test.ts index b7a1c1069c..bbbcd75bff 100644 --- a/cli/test/override_settings_behavior.test.ts +++ b/cli/test/override_settings_behavior.test.ts @@ -1,4 +1,5 @@ -import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; +import { writeFile } from "node:fs/promises"; import { getEffectiveSettings } from "../src/core/conf.ts"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; @@ -9,11 +10,7 @@ import { parseJsonFromCLIOutput } from "./test_config_helpers.ts"; // Tests for gitBranches override inheritance and file filtering behavior // ============================================================================= -Deno.test({ - name: "Override Settings: branch override inherits non-overridden settings from base config", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Override Settings: branch override inherits non-overridden settings from base config", async () => { const config = { includes: ["default/**"], skipVariables: true, // Base has this as true @@ -39,21 +36,16 @@ Deno.test({ ); // Override values should be used - assertEquals(effective.includes, ["override/**"], "Must use override includes"); - assertEquals(effective.skipApps, true, "Must use override skipApps"); + expect(effective.includes).toEqual(["override/**"]); + expect(effective.skipApps).toEqual(true); // Should inherit skip flags from base config - assertEquals(effective.skipVariables, true, "Must inherit skipVariables=true from base config"); - assertEquals(effective.skipResources, true, "Must inherit skipResources=true from base config"); - assertEquals(effective.defaultTs, "bun", "Must inherit defaultTs from base config"); - } + expect(effective.skipVariables).toEqual(true); + expect(effective.skipResources).toEqual(true); + expect(effective.defaultTs).toEqual("bun"); }); -Deno.test({ - name: "Override Settings: branch-specific settings take precedence", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Override Settings: branch-specific settings take precedence", async () => { const config = { includes: ["default/**"], skipVariables: false, @@ -81,8 +73,8 @@ Deno.test({ true, "main" ); - assertEquals(mainEffective.includes, ["main/**"], "Main branch must use its own includes"); - assertEquals(mainEffective.skipVariables, true, "Main branch must use its own skipVariables"); + expect(mainEffective.includes).toEqual(["main/**"]); + expect(mainEffective.skipVariables).toEqual(true); // Test dev branch const devEffective = await getEffectiveSettings( @@ -92,20 +84,15 @@ Deno.test({ true, "dev" ); - assertEquals(devEffective.includes, ["dev/**"], "Dev branch must use its own includes"); - assertEquals(devEffective.skipVariables, false, "Dev branch must use its own skipVariables"); - } + expect(devEffective.includes).toEqual(["dev/**"]); + expect(devEffective.skipVariables).toEqual(false); }); // ============================================================================= // INTEGRATION TESTS - File Filtering Behavior with gitBranches // ============================================================================= -Deno.test({ - name: "Integration: sync pull with skipVariables branch override excludes variable files", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Integration: sync pull with skipVariables branch override excludes variable files", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -117,7 +104,7 @@ Deno.test({ await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); // Create wmill.yaml with gitBranches override that skips variables - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" skipVariables: false @@ -125,7 +112,7 @@ skipVariables: false gitBranches: test_branch: overrides: - skipVariables: true`); + skipVariables: true`, "utf-8"); // Run sync pull with --branch to force using test_branch config const result = await backend.runCLICommand([ @@ -135,29 +122,24 @@ gitBranches: '--json-output' ], tempDir); - assertEquals(result.code, 0, `Sync pull should succeed: ${result.stderr}`); + expect(result.code).toEqual(0); // Parse output and verify variable files are NOT included const output = parseJsonFromCLIOutput(result.stdout); const changePaths = (output.changes || []).map((c: any) => c.path); const hasVariableFile = changePaths.some((path: string) => path.includes('.variable.yaml')); - assertEquals(hasVariableFile, false, "Variable files should NOT be included due to skipVariables override"); + expect(hasVariableFile).toEqual(false); // Verify other files ARE included const hasOtherFiles = changePaths.some((path: string) => !path.includes('.variable.yaml') && !path.includes('wmill.yaml') ); - assert(hasOtherFiles, `Other files should be included. Found paths: ${changePaths.join(', ')}`); + expect(hasOtherFiles).toBeTruthy(); }); - } }); -Deno.test({ - name: "Integration: sync pull respects includes branch override for file filtering", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Integration: sync pull respects includes branch override for file filtering", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -169,7 +151,7 @@ Deno.test({ await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); // Create wmill.yaml with gitBranches override for includes - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" @@ -178,7 +160,7 @@ gitBranches: overrides: includes: - "users/**" - - "groups/**"`); + - "groups/**"`, "utf-8"); // Run sync pull with --branch to use restricted includes const result = await backend.runCLICommand([ @@ -190,7 +172,7 @@ gitBranches: '--json-output' ], tempDir); - assertEquals(result.code, 0, `Sync pull should succeed: ${result.stderr}`); + expect(result.code).toEqual(0); // Parse output const output = parseJsonFromCLIOutput(result.stdout); @@ -202,20 +184,15 @@ gitBranches: const hasUserFiles = normalizedPaths.some((path: string) => path.includes('users/')); const hasGroupFiles = normalizedPaths.some((path: string) => path.includes('groups/')); - assert(hasUserFiles || hasGroupFiles, `User or group files should be included. Found: ${normalizedPaths.join(', ')}`); + expect(hasUserFiles || hasGroupFiles).toBeTruthy(); // Verify f/** files are NOT included (due to restrictive includes) const hasFolderFiles = normalizedPaths.some((path: string) => path.startsWith('f/')); - assertEquals(hasFolderFiles, false, `f/ files should NOT be included due to restrictive includes. Found: ${normalizedPaths.join(', ')}`); + expect(hasFolderFiles).toEqual(false); }); - } }); -Deno.test({ - name: "Integration: different branches have different settings", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Integration: different branches have different settings", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -227,7 +204,7 @@ Deno.test({ await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); // Create wmill.yaml with different settings per branch - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" skipVariables: false @@ -241,7 +218,7 @@ gitBranches: dev: overrides: skipVariables: false - skipResources: false`); + skipResources: false`, "utf-8"); // Test prod branch - should skip variables and resources const prodResult = await backend.runCLICommand([ @@ -251,7 +228,7 @@ gitBranches: '--json-output' ], tempDir); - assertEquals(prodResult.code, 0, `Prod sync pull should succeed: ${prodResult.stderr}`); + expect(prodResult.code).toEqual(0); const prodOutput = parseJsonFromCLIOutput(prodResult.stdout); const prodPaths = (prodOutput.changes || []).map((c: any) => c.path); @@ -259,8 +236,8 @@ gitBranches: const prodHasVariables = prodPaths.some((path: string) => path.includes('.variable.yaml')); const prodHasResources = prodPaths.some((path: string) => path.includes('.resource.yaml')); - assertEquals(prodHasVariables, false, "Prod branch should skip variables"); - assertEquals(prodHasResources, false, "Prod branch should skip resources"); + expect(prodHasVariables).toEqual(false); + expect(prodHasResources).toEqual(false); // Test dev branch - should include variables and resources const devResult = await backend.runCLICommand([ @@ -270,7 +247,7 @@ gitBranches: '--json-output' ], tempDir); - assertEquals(devResult.code, 0, `Dev sync pull should succeed: ${devResult.stderr}`); + expect(devResult.code).toEqual(0); const devOutput = parseJsonFromCLIOutput(devResult.stdout); const devPaths = (devOutput.changes || []).map((c: any) => c.path); @@ -278,8 +255,7 @@ gitBranches: const devHasVariables = devPaths.some((path: string) => path.includes('.variable.yaml')); const devHasResources = devPaths.some((path: string) => path.includes('.resource.yaml')); - assertEquals(devHasVariables, true, "Dev branch should include variables"); - assertEquals(devHasResources, true, "Dev branch should include resources"); + expect(devHasVariables).toEqual(true); + expect(devHasResources).toEqual(true); }); - } }); diff --git a/cli/test/preview.test.ts b/cli/test/preview.test.ts index eeb3718309..93dc7def48 100644 --- a/cli/test/preview.test.ts +++ b/cli/test/preview.test.ts @@ -1,5 +1,6 @@ -import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts"; -import { withTestBackend, cleanupTestBackend } from "./test_backend.ts"; +import { expect, test } from "bun:test"; +import { mkdir, writeFile } from "node:fs/promises"; +import { withTestBackend } from "./test_backend.ts"; // ============================================================================= // PREVIEW COMMAND INTEGRATION TESTS @@ -53,7 +54,7 @@ async function createWmillConfig( } } - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, yamlContent); + await writeFile(`${tempDir}/wmill.yaml`, yamlContent, "utf-8"); } // Helper to create a script file with metadata @@ -67,8 +68,8 @@ async function createScript( } ): Promise { const dir = `${tempDir}/${path.substring(0, path.lastIndexOf("/"))}`; - await Deno.mkdir(dir, { recursive: true }); - await Deno.writeTextFile(`${tempDir}/${path}`, content); + await mkdir(dir, { recursive: true }); + await writeFile(`${tempDir}/${path}`, content, "utf-8"); // Create metadata file const metaPath = path.replace(/\.[^.]+$/, ".script.yaml"); @@ -84,7 +85,7 @@ schema: default: "World" required: [] `; - await Deno.writeTextFile(`${tempDir}/${metaPath}`, metaContent); + await writeFile(`${tempDir}/${metaPath}`, metaContent, "utf-8"); } // Helper to create a flow directory with flow.yaml @@ -97,7 +98,7 @@ async function createFlow( } ): Promise { const dir = `${tempDir}/${flowPath}`; - await Deno.mkdir(dir, { recursive: true }); + await mkdir(dir, { recursive: true }); const flowYaml = `summary: "${options.summary}" description: "Test flow" @@ -118,125 +119,109 @@ schema: default: "World" required: [] `; - await Deno.writeTextFile(`${dir}/flow.yaml`, flowYaml); + await writeFile(`${dir}/flow.yaml`, flowYaml, "utf-8"); } // ============================================================================= // SCRIPT PREVIEW TESTS // ============================================================================= -Deno.test({ - name: "script preview: regular script (non-codebase)", - async fn() { - await withTestBackend(async (backend, tempDir) => { - await createWmillConfig(tempDir, { defaultTs: "bun" }); - await createScript( - tempDir, - "f/test/simple_script.ts", - `export function main(name: string = "World") { +test("script preview: regular script (non-codebase)", async () => { + await withTestBackend(async (backend, tempDir) => { + await createWmillConfig(tempDir, { defaultTs: "bun" }); + await createScript( + tempDir, + "f/test/simple_script.ts", + `export function main(name: string = "World") { return \`Hello, \${name}!\`; }` - ); + ); - const result = await backend.runCLICommand( - ["script", "preview", "f/test/simple_script.ts"], - tempDir - ); + const result = await backend.runCLICommand( + ["script", "preview", "f/test/simple_script.ts"], + tempDir + ); - assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`); - assertStringIncludes(result.stdout + result.stderr, "Hello, World!"); - }); - }, - sanitizeResources: false, - sanitizeOps: false, + expect(result.code).toEqual(0); + expect(result.stdout + result.stderr).toContain("Hello, World!"); + }); }); -Deno.test({ - name: "script preview: codebase script (CJS)", - async fn() { - await withTestBackend(async (backend, tempDir) => { - await createWmillConfig(tempDir, { - defaultTs: "bun", - codebases: [{ relative_path: "f/codebase", includes: ["**"] }], - }); +test("script preview: codebase script (CJS)", async () => { + await withTestBackend(async (backend, tempDir) => { + await createWmillConfig(tempDir, { + defaultTs: "bun", + codebases: [{ relative_path: "f/codebase", includes: ["**"] }], + }); - await createScript( - tempDir, - "f/codebase/cjs_script.ts", - `export function main(name: string = "World") { + await createScript( + tempDir, + "f/codebase/cjs_script.ts", + `export function main(name: string = "World") { console.log("CJS codebase script running"); return \`Hello from CJS codebase, \${name}!\`; }` - ); + ); - const result = await backend.runCLICommand( - ["script", "preview", "f/codebase/cjs_script.ts"], - tempDir - ); + const result = await backend.runCLICommand( + ["script", "preview", "f/codebase/cjs_script.ts"], + tempDir + ); - assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`); - assertStringIncludes(result.stdout + result.stderr, "Hello from CJS codebase, World!"); - }); - }, - sanitizeResources: false, - sanitizeOps: false, + expect(result.code).toEqual(0); + expect(result.stdout + result.stderr).toContain("Hello from CJS codebase, World!"); + }); }); -Deno.test({ - name: "script preview: codebase script (ESM)", - async fn() { - await withTestBackend(async (backend, tempDir) => { - await createWmillConfig(tempDir, { - defaultTs: "bun", - codebases: [{ relative_path: "f/codebase_esm", includes: ["**"], format: "esm" }], - }); +test("script preview: codebase script (ESM)", async () => { + await withTestBackend(async (backend, tempDir) => { + await createWmillConfig(tempDir, { + defaultTs: "bun", + codebases: [{ relative_path: "f/codebase_esm", includes: ["**"], format: "esm" }], + }); - await createScript( - tempDir, - "f/codebase_esm/esm_script.ts", - `export function main(name: string = "World") { + await createScript( + tempDir, + "f/codebase_esm/esm_script.ts", + `export function main(name: string = "World") { console.log("ESM codebase script running"); return \`Hello from ESM codebase, \${name}!\`; }` - ); + ); - const result = await backend.runCLICommand( - ["script", "preview", "f/codebase_esm/esm_script.ts"], - tempDir - ); + const result = await backend.runCLICommand( + ["script", "preview", "f/codebase_esm/esm_script.ts"], + tempDir + ); - assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`); - assertStringIncludes(result.stdout + result.stderr, "Hello from ESM codebase, World!"); - }); - }, - sanitizeResources: false, - sanitizeOps: false, + expect(result.code).toEqual(0); + expect(result.stdout + result.stderr).toContain("Hello from ESM codebase, World!"); + }); }); -Deno.test({ - name: "script preview: codebase script with assets (tar)", - async fn() { - await withTestBackend(async (backend, tempDir) => { - await createWmillConfig(tempDir, { - defaultTs: "bun", - codebases: [{ - relative_path: "f/codebase_tar", - includes: ["**"], - assets: [{ from: "f/codebase_tar/data.json", to: "data.json" }], - }], - }); +test("script preview: codebase script with assets (tar)", async () => { + await withTestBackend(async (backend, tempDir) => { + await createWmillConfig(tempDir, { + defaultTs: "bun", + codebases: [{ + relative_path: "f/codebase_tar", + includes: ["**"], + assets: [{ from: "f/codebase_tar/data.json", to: "data.json" }], + }], + }); - // Create asset file - await Deno.mkdir(`${tempDir}/f/codebase_tar`, { recursive: true }); - await Deno.writeTextFile( - `${tempDir}/f/codebase_tar/data.json`, - JSON.stringify({ message: "Hello from asset!" }) - ); + // Create asset file + await mkdir(`${tempDir}/f/codebase_tar`, { recursive: true }); + await writeFile( + `${tempDir}/f/codebase_tar/data.json`, + JSON.stringify({ message: "Hello from asset!" }), + "utf-8" + ); - await createScript( - tempDir, - "f/codebase_tar/tar_script.ts", - `import * as fs from "fs"; + await createScript( + tempDir, + "f/codebase_tar/tar_script.ts", + `import * as fs from "fs"; export function main(name: string = "World") { console.log("Tar codebase script running"); @@ -244,46 +229,42 @@ export function main(name: string = "World") { const parsed = JSON.parse(data); return \`Hello \${name}! Asset says: \${parsed.message}\`; }` - ); + ); - const result = await backend.runCLICommand( - ["script", "preview", "f/codebase_tar/tar_script.ts"], - tempDir - ); + const result = await backend.runCLICommand( + ["script", "preview", "f/codebase_tar/tar_script.ts"], + tempDir + ); - assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`); - assertStringIncludes(result.stdout + result.stderr, "Hello World! Asset says: Hello from asset!"); - }); - }, - sanitizeResources: false, - sanitizeOps: false, + expect(result.code).toEqual(0); + expect(result.stdout + result.stderr).toContain("Hello World! Asset says: Hello from asset!"); + }); }); -Deno.test({ - name: "script preview: codebase script ESM + tar (assets)", - async fn() { - await withTestBackend(async (backend, tempDir) => { - await createWmillConfig(tempDir, { - defaultTs: "bun", - codebases: [{ - relative_path: "f/codebase_esm_tar", - includes: ["**"], - format: "esm", - assets: [{ from: "f/codebase_esm_tar/config.json", to: "config.json" }], - }], - }); +test("script preview: codebase script ESM + tar (assets)", async () => { + await withTestBackend(async (backend, tempDir) => { + await createWmillConfig(tempDir, { + defaultTs: "bun", + codebases: [{ + relative_path: "f/codebase_esm_tar", + includes: ["**"], + format: "esm", + assets: [{ from: "f/codebase_esm_tar/config.json", to: "config.json" }], + }], + }); - // Create asset file - await Deno.mkdir(`${tempDir}/f/codebase_esm_tar`, { recursive: true }); - await Deno.writeTextFile( - `${tempDir}/f/codebase_esm_tar/config.json`, - JSON.stringify({ setting: "esm_tar_value" }) - ); + // Create asset file + await mkdir(`${tempDir}/f/codebase_esm_tar`, { recursive: true }); + await writeFile( + `${tempDir}/f/codebase_esm_tar/config.json`, + JSON.stringify({ setting: "esm_tar_value" }), + "utf-8" + ); - await createScript( - tempDir, - "f/codebase_esm_tar/esm_tar_script.ts", - `import * as fs from "fs"; + await createScript( + tempDir, + "f/codebase_esm_tar/esm_tar_script.ts", + `import * as fs from "fs"; export function main(name: string = "World") { console.log("ESM + tar codebase script running"); @@ -291,68 +272,65 @@ export function main(name: string = "World") { const parsed = JSON.parse(config); return \`Hello \${name}! Config setting: \${parsed.setting}\`; }` - ); + ); - const result = await backend.runCLICommand( - ["script", "preview", "f/codebase_esm_tar/esm_tar_script.ts"], - tempDir - ); + const result = await backend.runCLICommand( + ["script", "preview", "f/codebase_esm_tar/esm_tar_script.ts"], + tempDir + ); - assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`); - assertStringIncludes(result.stdout + result.stderr, "Hello World! Config setting: esm_tar_value"); - }); - }, - sanitizeResources: false, - sanitizeOps: false, + expect(result.code).toEqual(0); + expect(result.stdout + result.stderr).toContain("Hello World! Config setting: esm_tar_value"); + }); }); -Deno.test({ - name: "script preview: codebase with imports (simulates ../shared layout)", - async fn() { - await withTestBackend(async (backend, tempDir) => { - // This test simulates a codebase that could be in a parent directory. - // The structure is: - // tempDir/ - // wmill.yaml (codebase at ".") - // f/ - // lib/ - // helper.ts (shared module) - // main_script.ts (imports helper) - // - // This tests that codebase bundling correctly includes imported modules, - // which is the key functionality needed for ../shared codebases during sync. - // Note: Preview requires valid windmill paths (u/, g/, f/), so we run - // from within the codebase directory. +test("script preview: codebase with imports (simulates ../shared layout)", async () => { + await withTestBackend(async (backend, tempDir) => { + // This test simulates a codebase that could be in a parent directory. + // The structure is: + // tempDir/ + // wmill.yaml (codebase at ".") + // f/ + // lib/ + // helper.ts (shared module) + // main_script.ts (imports helper) + // + // This tests that codebase bundling correctly includes imported modules, + // which is the key functionality needed for ../shared codebases during sync. + // Note: Preview requires valid windmill paths (u/, g/, f/), so we run + // from within the codebase directory. - await createWmillConfig(tempDir, { - defaultTs: "bun", - codebases: [{ relative_path: ".", includes: ["**"] }], - }); + await createWmillConfig(tempDir, { + defaultTs: "bun", + codebases: [{ relative_path: ".", includes: ["**"] }], + }); - // Create helper module - await Deno.mkdir(`${tempDir}/f/lib`, { recursive: true }); - await Deno.writeTextFile( - `${tempDir}/f/lib/helper.ts`, - `export function greet(name: string): string { + // Create helper module + await mkdir(`${tempDir}/f/lib`, { recursive: true }); + await writeFile( + `${tempDir}/f/lib/helper.ts`, + `export function greet(name: string): string { return \`Hello from shared codebase, \${name}!\`; -}` - ); +}`, + "utf-8" + ); - // Create main script that imports the helper - await Deno.writeTextFile( - `${tempDir}/f/lib/main_script.ts`, - `import { greet } from "./helper"; + // Create main script that imports the helper + await writeFile( + `${tempDir}/f/lib/main_script.ts`, + `import { greet } from "./helper"; export function main(name: string = "World") { console.log("Running codebase script with imports"); return greet(name); -}` - ); +}`, + "utf-8" + ); - // Create script metadata - await Deno.writeTextFile( - `${tempDir}/f/lib/main_script.script.yaml`, - `summary: "Test script with imports" + // Create script metadata + await writeFile( + `${tempDir}/f/lib/main_script.script.yaml`, + `summary: "Test script with imports" description: "Test script that imports from helper module" lock: "" schema: @@ -363,64 +341,43 @@ schema: type: string default: "World" required: [] -` - ); +`, + "utf-8" + ); - // Run preview - the script should be bundled with the helper module - const result = await backend.runCLICommand( - ["script", "preview", "f/lib/main_script.ts"], - tempDir - ); + // Run preview - the script should be bundled with the helper module + const result = await backend.runCLICommand( + ["script", "preview", "f/lib/main_script.ts"], + tempDir + ); - assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`); - // The script should be bundled (includes the helper) and run successfully - assertStringIncludes( - result.stdout + result.stderr, - "Hello from shared codebase, World!", - `Expected codebase script output not found. Got: ${result.stdout}\n${result.stderr}` - ); - }); - }, - sanitizeResources: false, - sanitizeOps: false, + expect(result.code).toEqual(0); + // The script should be bundled (includes the helper) and run successfully + expect( + result.stdout + result.stderr, + ).toContain("Hello from shared codebase, World!"); + }); }); // ============================================================================= // FLOW PREVIEW TESTS // ============================================================================= -Deno.test({ - name: "flow preview: simple flow", - async fn() { - await withTestBackend(async (backend, tempDir) => { - await createWmillConfig(tempDir, { defaultTs: "bun" }); - await createFlow(tempDir, "f/test/simple_flow.flow", { - summary: "Test flow", - scriptContent: `export function main(name: string = "World") { return \`Flow says: Hello, \${name}!\`; }`, - }); - - const result = await backend.runCLICommand( - ["flow", "preview", "f/test/simple_flow.flow"], - tempDir - ); - - assertEquals(result.code, 0, `Flow preview failed: ${result.stderr}\n${result.stdout}`); - assertStringIncludes(result.stdout + result.stderr, "Flow says: Hello, World!"); +test("flow preview: simple flow", async () => { + await withTestBackend(async (backend, tempDir) => { + await createWmillConfig(tempDir, { defaultTs: "bun" }); + await createFlow(tempDir, "f/test/simple_flow.flow", { + summary: "Test flow", + scriptContent: `export function main(name: string = "World") { return \`Flow says: Hello, \${name}!\`; }`, }); - }, - sanitizeResources: false, - sanitizeOps: false, + + const result = await backend.runCLICommand( + ["flow", "preview", "f/test/simple_flow.flow"], + tempDir + ); + + expect(result.code).toEqual(0); + expect(result.stdout + result.stderr).toContain("Flow says: Hello, World!"); + }); }); -// ============================================================================= -// CLEANUP -// ============================================================================= - -Deno.test({ - name: "cleanup test backend", - async fn() { - await cleanupTestBackend(); - }, - sanitizeResources: false, - sanitizeOps: false, -}); diff --git a/cli/test/raw_app_sync.test.ts b/cli/test/raw_app_sync.test.ts index 8f9d19b588..e9151b45fc 100644 --- a/cli/test/raw_app_sync.test.ts +++ b/cli/test/raw_app_sync.test.ts @@ -1,8 +1,8 @@ -import { assertEquals, assert, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; -import * as path from "https://deno.land/std@0.224.0/path/mod.ts"; -import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts"; +import * as path from "@std/path"; +import { writeFile, readFile, stat, rm, mkdir } from "node:fs/promises"; // ============================================================================= // RAW APP SYNC TESTS @@ -92,7 +92,7 @@ policy: async function fileExists(filePath: string): Promise { try { - await Deno.stat(filePath); + await stat(filePath); return true; } catch { return false; @@ -100,7 +100,7 @@ async function fileExists(filePath: string): Promise { } async function readFileContent(filePath: string): Promise { - return await Deno.readTextFile(filePath); + return await readFile(filePath, "utf-8"); } /** @@ -108,35 +108,32 @@ async function readFileContent(filePath: string): Promise { * Uses .raw_app folder suffix with raw_app.yaml metadata */ async function createRawAppOnDisk(appDir: string): Promise { - await ensureDir(appDir); - await ensureDir(path.join(appDir, "inline_scripts")); + await mkdir(appDir, { recursive: true }); + await mkdir(path.join(appDir, "inline_scripts"), { recursive: true }); // Create raw_app.yaml metadata file - await Deno.writeTextFile(path.join(appDir, "raw_app.yaml"), RAW_APP_YAML); + await writeFile(path.join(appDir, "raw_app.yaml"), RAW_APP_YAML, "utf-8"); // Create app source files - await Deno.writeTextFile(path.join(appDir, "App.tsx"), APP_TSX); - await Deno.writeTextFile(path.join(appDir, "index.css"), INDEX_CSS); - await Deno.writeTextFile(path.join(appDir, "index.tsx"), INDEX_TSX); - await Deno.writeTextFile(path.join(appDir, "package.json"), PACKAGE_JSON); + await writeFile(path.join(appDir, "App.tsx"), APP_TSX, "utf-8"); + await writeFile(path.join(appDir, "index.css"), INDEX_CSS, "utf-8"); + await writeFile(path.join(appDir, "index.tsx"), INDEX_TSX, "utf-8"); + await writeFile(path.join(appDir, "package.json"), PACKAGE_JSON, "utf-8"); // Create inline script in inline_scripts folder - await Deno.writeTextFile( + await writeFile( path.join(appDir, "inline_scripts", "a.inline_script.ts"), - INLINE_SCRIPT_A + INLINE_SCRIPT_A, + "utf-8" ); - await Deno.writeTextFile( + await writeFile( path.join(appDir, "inline_scripts", "a.inline_script.lock"), - INLINE_SCRIPT_A_LOCK + INLINE_SCRIPT_A_LOCK, + "utf-8" ); } -Deno.test({ - name: "Raw App: full sync workflow - push, pull, modify, push, clear, pull", - ignore: false, - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Raw App: full sync workflow - push, pull, modify, push, clear, pull", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -148,14 +145,14 @@ Deno.test({ await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); // Create wmill.yaml - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" -excludes: []`); +excludes: []`, "utf-8"); // Create folder structure const appDir = path.join(tempDir, "f", "test", "my_raw_app.raw_app"); - await ensureDir(path.join(tempDir, "f", "test")); + await mkdir(path.join(tempDir, "f", "test"), { recursive: true }); await createRawAppOnDisk(appDir); // ========================================================================= @@ -166,23 +163,23 @@ excludes: []`); '--yes' ], tempDir, "raw_app_test"); - assertEquals(pushResult1.code, 0, `Initial sync push should succeed: ${pushResult1.stderr}`); + expect(pushResult1.code).toEqual(0); // ========================================================================= // STEP 2: Clear disk and pull - verify raw app is pulled correctly // ========================================================================= - await Deno.remove(appDir, { recursive: true }); - assert(!(await fileExists(appDir)), "App directory should be deleted before pull"); + await rm(appDir, { recursive: true }); + expect(!(await fileExists(appDir))).toBeTruthy(); const pullResult1 = await backend.runCLICommand([ 'sync', 'pull', '--yes' ], tempDir, "raw_app_test"); - assertEquals(pullResult1.code, 0, `Sync pull should succeed: ${pullResult1.stderr}`); + expect(pullResult1.code).toEqual(0); // Verify raw app directory structure was created - assert(await fileExists(appDir), `Raw app directory should exist at ${appDir}`); + expect(await fileExists(appDir)).toBeTruthy(); // Verify files were pulled const appTsxPath = path.join(appDir, "App.tsx"); @@ -191,22 +188,22 @@ excludes: []`); const packageJsonPath = path.join(appDir, "package.json"); const inlineScriptPath = path.join(appDir, "inline_scripts", "a.inline_script.ts"); - assert(await fileExists(appTsxPath), "App.tsx should exist"); - assert(await fileExists(indexCssPath), "index.css should exist"); - assert(await fileExists(indexTsxPath), "index.tsx should exist"); - assert(await fileExists(packageJsonPath), "package.json should exist"); - assert(await fileExists(inlineScriptPath), "Inline script a.inline_script.ts should exist"); + expect(await fileExists(appTsxPath)).toBeTruthy(); + expect(await fileExists(indexCssPath)).toBeTruthy(); + expect(await fileExists(indexTsxPath)).toBeTruthy(); + expect(await fileExists(packageJsonPath)).toBeTruthy(); + expect(await fileExists(inlineScriptPath)).toBeTruthy(); // Verify file contents const appTsxContent = await readFileContent(appTsxPath); - assertStringIncludes(appTsxContent, "hello world", "App.tsx should contain 'hello world'"); - assertStringIncludes(appTsxContent, "backend.a", "App.tsx should reference backend.a"); + expect(appTsxContent).toContain("hello world"); + expect(appTsxContent).toContain("backend.a"); const indexCssContent = await readFileContent(indexCssPath); - assertStringIncludes(indexCssContent, ".myclass", "index.css should contain .myclass"); + expect(indexCssContent).toContain(".myclass"); const inlineScriptContent = await readFileContent(inlineScriptPath); - assertStringIncludes(inlineScriptContent, "export async function main", "Inline script should have main function"); + expect(inlineScriptContent).toContain("export async function main"); // ========================================================================= // STEP 3: Modify files locally @@ -214,15 +211,15 @@ excludes: []`); // Modify App.tsx - change the heading const modifiedAppTsx = appTsxContent.replace("hello world", "hello modified world"); - await Deno.writeTextFile(appTsxPath, modifiedAppTsx); + await writeFile(appTsxPath, modifiedAppTsx, "utf-8"); // Modify index.css - change the border color const modifiedIndexCss = indexCssContent.replace("gray", "blue"); - await Deno.writeTextFile(indexCssPath, modifiedIndexCss); + await writeFile(indexCssPath, modifiedIndexCss, "utf-8"); // Modify inline script - change the return value const modifiedInlineScript = inlineScriptContent.replace("return x", "return `modified: ${x}`"); - await Deno.writeTextFile(inlineScriptPath, modifiedInlineScript); + await writeFile(inlineScriptPath, modifiedInlineScript, "utf-8"); // ========================================================================= // STEP 4: Push changes @@ -232,13 +229,13 @@ excludes: []`); '--yes' ], tempDir, "raw_app_test"); - assertEquals(pushResult2.code, 0, `Sync push should succeed: ${pushResult2.stderr}`); + expect(pushResult2.code).toEqual(0); // ========================================================================= // STEP 5: Clear disk (delete the app directory) // ========================================================================= - await Deno.remove(appDir, { recursive: true }); - assert(!(await fileExists(appDir)), "App directory should be deleted"); + await rm(appDir, { recursive: true }); + expect(!(await fileExists(appDir))).toBeTruthy(); // ========================================================================= // STEP 6: Pull again and verify modifications persisted @@ -248,37 +245,31 @@ excludes: []`); '--yes' ], tempDir, "raw_app_test"); - assertEquals(pullResult2.code, 0, `Second sync pull should succeed: ${pullResult2.stderr}`); + expect(pullResult2.code).toEqual(0); // Verify app directory exists again - assert(await fileExists(appDir), "Raw app directory should exist after second pull"); + expect(await fileExists(appDir)).toBeTruthy(); // Verify all files were pulled again - assert(await fileExists(appTsxPath), "App.tsx should exist after second pull"); - assert(await fileExists(indexCssPath), "index.css should exist after second pull"); - assert(await fileExists(indexTsxPath), "index.tsx should exist after second pull"); - assert(await fileExists(packageJsonPath), "package.json should exist after second pull"); - assert(await fileExists(inlineScriptPath), "Inline script should exist after second pull"); + expect(await fileExists(appTsxPath)).toBeTruthy(); + expect(await fileExists(indexCssPath)).toBeTruthy(); + expect(await fileExists(indexTsxPath)).toBeTruthy(); + expect(await fileExists(packageJsonPath)).toBeTruthy(); + expect(await fileExists(inlineScriptPath)).toBeTruthy(); // Verify modifications were persisted const pulledAppTsx = await readFileContent(appTsxPath); - assertStringIncludes(pulledAppTsx, "hello modified world", "Modifications to App.tsx should persist"); + expect(pulledAppTsx).toContain("hello modified world"); const pulledIndexCss = await readFileContent(indexCssPath); - assertStringIncludes(pulledIndexCss, "blue", "Modifications to index.css should persist"); + expect(pulledIndexCss).toContain("blue"); const pulledInlineScript = await readFileContent(inlineScriptPath); - assertStringIncludes(pulledInlineScript, "modified:", "Modifications to inline script should persist"); + expect(pulledInlineScript).toContain("modified:"); }); - } }); -Deno.test({ - name: "Raw App: add new file and push", - ignore: false, - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Raw App: add new file and push", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -290,14 +281,14 @@ Deno.test({ await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); // Create wmill.yaml - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" -excludes: []`); +excludes: []`, "utf-8"); // Create initial raw app const appDir = path.join(tempDir, "f", "test", "new_file_app.raw_app"); - await ensureDir(path.join(tempDir, "f", "test")); + await mkdir(path.join(tempDir, "f", "test"), { recursive: true }); await createRawAppOnDisk(appDir); // Initial push @@ -306,14 +297,14 @@ excludes: []`); '--yes' ], tempDir, "raw_app_new_file_test"); - assertEquals(pushResult1.code, 0, `Initial sync push should succeed: ${pushResult1.stderr}`); + expect(pushResult1.code).toEqual(0); // Add a new file const newFilePath = path.join(appDir, "utils.ts"); - await Deno.writeTextFile(newFilePath, `export function formatValue(val: string): string { + await writeFile(newFilePath, `export function formatValue(val: string): string { return \`Formatted: \${val}\`; } -`); +`, "utf-8"); // Push changes const pushResult2 = await backend.runCLICommand([ @@ -321,32 +312,26 @@ excludes: []`); '--yes' ], tempDir, "raw_app_new_file_test"); - assertEquals(pushResult2.code, 0, `Sync push with new file should succeed: ${pushResult2.stderr}`); + expect(pushResult2.code).toEqual(0); // Clear and pull again - await Deno.remove(appDir, { recursive: true }); + await rm(appDir, { recursive: true }); const pullResult = await backend.runCLICommand([ 'sync', 'pull', '--yes' ], tempDir, "raw_app_new_file_test"); - assertEquals(pullResult.code, 0, `Sync pull should succeed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Verify new file was persisted - assert(await fileExists(newFilePath), "New file utils.ts should exist after pull"); + expect(await fileExists(newFilePath)).toBeTruthy(); const newFileContent = await readFileContent(newFilePath); - assertStringIncludes(newFileContent, "formatValue", "New file content should persist"); + expect(newFileContent).toContain("formatValue"); }); - } }); -Deno.test({ - name: "Raw App: delete file and push", - ignore: false, - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Raw App: delete file and push", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -358,14 +343,14 @@ Deno.test({ await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); // Create wmill.yaml - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" -excludes: []`); +excludes: []`, "utf-8"); // Create initial raw app const appDir = path.join(tempDir, "f", "test", "delete_file_app.raw_app"); - await ensureDir(path.join(tempDir, "f", "test")); + await mkdir(path.join(tempDir, "f", "test"), { recursive: true }); await createRawAppOnDisk(appDir); // Initial push @@ -374,20 +359,20 @@ excludes: []`); '--yes' ], tempDir, "raw_app_delete_file_test"); - assertEquals(pushResult1.code, 0, `Initial sync push should succeed: ${pushResult1.stderr}`); + expect(pushResult1.code).toEqual(0); const indexCssPath = path.join(appDir, "index.css"); const appTsxPath = path.join(appDir, "App.tsx"); - assert(await fileExists(indexCssPath), "index.css should exist after initial push"); + expect(await fileExists(indexCssPath)).toBeTruthy(); // First, update App.tsx to remove the CSS import (otherwise bundle will fail) const appTsxContent = await readFileContent(appTsxPath); const updatedAppTsx = appTsxContent.replace("import './index.css'\n", ""); - await Deno.writeTextFile(appTsxPath, updatedAppTsx); + await writeFile(appTsxPath, updatedAppTsx, "utf-8"); // Delete the CSS file - await Deno.remove(indexCssPath); - assert(!(await fileExists(indexCssPath)), "index.css should be deleted locally"); + await rm(indexCssPath); + expect(!(await fileExists(indexCssPath))).toBeTruthy(); // Push changes const pushResult2 = await backend.runCLICommand([ @@ -395,33 +380,27 @@ excludes: []`); '--yes' ], tempDir, "raw_app_delete_file_test"); - assertEquals(pushResult2.code, 0, `Sync push after delete should succeed: ${pushResult2.stderr}`); + expect(pushResult2.code).toEqual(0); // Clear and pull again - await Deno.remove(appDir, { recursive: true }); + await rm(appDir, { recursive: true }); const pullResult = await backend.runCLICommand([ 'sync', 'pull', '--yes' ], tempDir, "raw_app_delete_file_test"); - assertEquals(pullResult.code, 0, `Sync pull should succeed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Verify the deleted file is NOT pulled (it was deleted from backend) - assert(!(await fileExists(indexCssPath)), "Deleted index.css should not exist after pull"); + expect(!(await fileExists(indexCssPath))).toBeTruthy(); // But other files should still exist - assert(await fileExists(appTsxPath), "App.tsx should still exist after pull"); + expect(await fileExists(appTsxPath)).toBeTruthy(); }); - } }); -Deno.test({ - name: "Raw App: dry-run push shows expected changes", - ignore: false, - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Raw App: dry-run push shows expected changes", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -433,14 +412,14 @@ Deno.test({ await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); // Create wmill.yaml - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" -excludes: []`); +excludes: []`, "utf-8"); // Create raw app const appDir = path.join(tempDir, "f", "test", "dry_run_app.raw_app"); - await ensureDir(path.join(tempDir, "f", "test")); + await mkdir(path.join(tempDir, "f", "test"), { recursive: true }); await createRawAppOnDisk(appDir); // Dry-run push @@ -450,7 +429,7 @@ excludes: []`); '--json-output' ], tempDir, "raw_app_dry_run_test"); - assertEquals(dryRunResult.code, 0, `Dry-run push should succeed: ${dryRunResult.stderr}`); + expect(dryRunResult.code).toEqual(0); // Parse JSON output (may be pretty-printed across multiple lines) let jsonOutput = null; @@ -469,13 +448,12 @@ excludes: []`); } } - assert(jsonOutput !== null, `Should have JSON output. Got: ${dryRunResult.stdout}`); - assert(Array.isArray(jsonOutput.changes), `Should have changes array. Got: ${JSON.stringify(jsonOutput)}`); + expect(jsonOutput !== null).toBeTruthy(); + expect(Array.isArray(jsonOutput.changes)).toBeTruthy(); // Should include raw app in changes const changePaths = jsonOutput.changes.map((c: any) => c.path); const hasRawApp = changePaths.some((p: string) => p.includes("dry_run_app")); - assert(hasRawApp, `Dry-run should show raw app. Found: ${changePaths.join(', ')}`); + expect(hasRawApp).toBeTruthy(); }); - } }); diff --git a/cli/test/resource_folders_unit.test.ts b/cli/test/resource_folders_unit.test.ts new file mode 100644 index 0000000000..cbcf6d72ea --- /dev/null +++ b/cli/test/resource_folders_unit.test.ts @@ -0,0 +1,525 @@ +/** + * Unit tests for resource_folders.ts path detection and manipulation functions. + * Tests both dotted (.flow, .app, .raw_app) and non-dotted (__flow, __app, __raw_app) modes. + */ + +import { expect, test, describe, beforeEach } from "bun:test"; +import { + setNonDottedPaths, + getNonDottedPaths, + getFolderSuffixes, + getFolderSuffix, + getMetadataFileName, + getMetadataPathSuffix, + isFlowPath, + isAppPath, + isRawAppPath, + isFolderResourcePath, + detectFolderResourceType, + isRawAppBackendPath, + isAppInlineScriptPath, + isFlowInlineScriptPath, + extractResourceName, + extractFolderPath, + buildFolderPath, + buildMetadataPath, + hasFolderSuffix, + validateFolderName, + extractNameFromFolder, + isFlowMetadataFile, + isAppMetadataFile, + isRawAppMetadataFile, + isRawAppFolderMetadataFile, + getDeleteSuffix, + transformJsonPathToDir, +} from "../src/utils/resource_folders.ts"; +import { removeWorkerPrefix } from "../src/commands/worker-groups/worker-groups.ts"; + +// ============================================================================= +// Helper: reset to dotted mode before each test +// ============================================================================= + +beforeEach(() => { + setNonDottedPaths(false); +}); + +// ============================================================================= +// Configuration Functions +// ============================================================================= + +describe("setNonDottedPaths / getNonDottedPaths", () => { + test("defaults to false (dotted)", () => { + expect(getNonDottedPaths()).toBe(false); + }); + + test("can be set to true", () => { + setNonDottedPaths(true); + expect(getNonDottedPaths()).toBe(true); + }); + + test("can be toggled back to false", () => { + setNonDottedPaths(true); + setNonDottedPaths(false); + expect(getNonDottedPaths()).toBe(false); + }); +}); + +describe("getFolderSuffixes", () => { + test("returns dotted suffixes by default", () => { + const suffixes = getFolderSuffixes(); + expect(suffixes.flow).toBe(".flow"); + expect(suffixes.app).toBe(".app"); + expect(suffixes.raw_app).toBe(".raw_app"); + }); + + test("returns non-dotted suffixes when configured", () => { + setNonDottedPaths(true); + const suffixes = getFolderSuffixes(); + expect(suffixes.flow).toBe("__flow"); + expect(suffixes.app).toBe("__app"); + expect(suffixes.raw_app).toBe("__raw_app"); + }); +}); + +describe("getFolderSuffix", () => { + test("returns correct suffix for each type (dotted)", () => { + expect(getFolderSuffix("flow")).toBe(".flow"); + expect(getFolderSuffix("app")).toBe(".app"); + expect(getFolderSuffix("raw_app")).toBe(".raw_app"); + }); + + test("returns correct suffix for each type (non-dotted)", () => { + setNonDottedPaths(true); + expect(getFolderSuffix("flow")).toBe("__flow"); + expect(getFolderSuffix("app")).toBe("__app"); + expect(getFolderSuffix("raw_app")).toBe("__raw_app"); + }); +}); + +// ============================================================================= +// Metadata File Names +// ============================================================================= + +describe("getMetadataFileName", () => { + test("returns correct metadata file names", () => { + expect(getMetadataFileName("flow", "yaml")).toBe("flow.yaml"); + expect(getMetadataFileName("flow", "json")).toBe("flow.json"); + expect(getMetadataFileName("app", "yaml")).toBe("app.yaml"); + expect(getMetadataFileName("app", "json")).toBe("app.json"); + expect(getMetadataFileName("raw_app", "yaml")).toBe("raw_app.yaml"); + expect(getMetadataFileName("raw_app", "json")).toBe("raw_app.json"); + }); +}); + +describe("getMetadataPathSuffix", () => { + test("returns correct path suffix (dotted)", () => { + expect(getMetadataPathSuffix("flow", "yaml")).toBe(".flow/flow.yaml"); + expect(getMetadataPathSuffix("app", "json")).toBe(".app/app.json"); + expect(getMetadataPathSuffix("raw_app", "yaml")).toBe(".raw_app/raw_app.yaml"); + }); + + test("returns correct path suffix (non-dotted)", () => { + setNonDottedPaths(true); + expect(getMetadataPathSuffix("flow", "yaml")).toBe("__flow/flow.yaml"); + expect(getMetadataPathSuffix("app", "json")).toBe("__app/app.json"); + expect(getMetadataPathSuffix("raw_app", "yaml")).toBe("__raw_app/raw_app.yaml"); + }); +}); + +// ============================================================================= +// Path Detection Functions (dotted mode) +// ============================================================================= + +describe("isFlowPath (dotted)", () => { + test("detects flow paths", () => { + expect(isFlowPath("f/my_flow.flow/flow.yaml")).toBe(true); + expect(isFlowPath("u/admin/test.flow/step.ts")).toBe(true); + }); + + test("rejects non-flow paths", () => { + expect(isFlowPath("f/my_script.ts")).toBe(false); + expect(isFlowPath("f/my_app.app/app.yaml")).toBe(false); + }); +}); + +describe("isAppPath (dotted)", () => { + test("detects app paths", () => { + expect(isAppPath("f/my_app.app/app.yaml")).toBe(true); + expect(isAppPath("u/admin/dashboard.app/inline.ts")).toBe(true); + }); + + test("rejects non-app paths", () => { + expect(isAppPath("f/my_script.ts")).toBe(false); + expect(isAppPath("f/my_flow.flow/flow.yaml")).toBe(false); + }); +}); + +describe("isRawAppPath (dotted)", () => { + test("detects raw_app paths", () => { + expect(isRawAppPath("f/my_raw.raw_app/raw_app.yaml")).toBe(true); + }); + + test("rejects non-raw_app paths", () => { + expect(isRawAppPath("f/my_app.app/app.yaml")).toBe(false); + expect(isRawAppPath("f/my_script.ts")).toBe(false); + }); +}); + +// ============================================================================= +// Path Detection Functions (non-dotted mode) +// ============================================================================= + +describe("isFlowPath (non-dotted)", () => { + test("detects non-dotted flow paths", () => { + setNonDottedPaths(true); + expect(isFlowPath("f/my_flow__flow/flow.yaml")).toBe(true); + }); + + test("rejects dotted flow paths in non-dotted mode", () => { + setNonDottedPaths(true); + expect(isFlowPath("f/my_flow.flow/flow.yaml")).toBe(false); + }); +}); + +describe("isAppPath (non-dotted)", () => { + test("detects non-dotted app paths", () => { + setNonDottedPaths(true); + expect(isAppPath("f/my_app__app/app.yaml")).toBe(true); + }); +}); + +describe("isRawAppPath (non-dotted)", () => { + test("detects non-dotted raw_app paths", () => { + setNonDottedPaths(true); + expect(isRawAppPath("f/my_raw__raw_app/raw_app.yaml")).toBe(true); + }); +}); + +// ============================================================================= +// Composite Path Detection +// ============================================================================= + +describe("isFolderResourcePath", () => { + test("returns true for any folder resource path", () => { + expect(isFolderResourcePath("f/x.flow/flow.yaml")).toBe(true); + expect(isFolderResourcePath("f/x.app/app.yaml")).toBe(true); + expect(isFolderResourcePath("f/x.raw_app/raw_app.yaml")).toBe(true); + }); + + test("returns false for non-folder paths", () => { + expect(isFolderResourcePath("f/script.ts")).toBe(false); + expect(isFolderResourcePath("f/var.variable.yaml")).toBe(false); + }); +}); + +describe("detectFolderResourceType", () => { + test("detects flow type", () => { + expect(detectFolderResourceType("f/x.flow/flow.yaml")).toBe("flow"); + }); + + test("detects app type", () => { + expect(detectFolderResourceType("f/x.app/app.yaml")).toBe("app"); + }); + + test("detects raw_app type", () => { + expect(detectFolderResourceType("f/x.raw_app/raw_app.yaml")).toBe("raw_app"); + }); + + test("returns null for non-folder paths", () => { + expect(detectFolderResourceType("f/script.ts")).toBeNull(); + }); +}); + +// ============================================================================= +// Inline Script / Backend Path Detection +// ============================================================================= + +describe("isRawAppBackendPath", () => { + test("detects raw app backend paths (dotted)", () => { + expect(isRawAppBackendPath("f/my_app.raw_app/backend/handler.ts")).toBe(true); + }); + + test("rejects non-backend raw app paths", () => { + expect(isRawAppBackendPath("f/my_app.raw_app/raw_app.yaml")).toBe(false); + }); + + test("detects raw app backend paths (non-dotted)", () => { + setNonDottedPaths(true); + expect(isRawAppBackendPath("f/my_app__raw_app/backend/handler.ts")).toBe(true); + }); +}); + +describe("isAppInlineScriptPath", () => { + test("detects inline script paths in apps", () => { + expect(isAppInlineScriptPath("f/dashboard.app/inline_0.ts")).toBe(true); + }); + + test("rejects non-app paths", () => { + expect(isAppInlineScriptPath("f/script.ts")).toBe(false); + }); +}); + +describe("isFlowInlineScriptPath", () => { + test("detects inline script paths in flows", () => { + expect(isFlowInlineScriptPath("f/pipeline.flow/step_0.ts")).toBe(true); + }); + + test("rejects non-flow paths", () => { + expect(isFlowInlineScriptPath("f/script.ts")).toBe(false); + }); +}); + +// ============================================================================= +// Path Manipulation Functions +// ============================================================================= + +describe("extractResourceName", () => { + test("extracts name from flow path", () => { + expect(extractResourceName("f/my_flow.flow/flow.yaml", "flow")).toBe("f/my_flow"); + }); + + test("extracts name from app path", () => { + expect(extractResourceName("f/dashboard.app/app.yaml", "app")).toBe("f/dashboard"); + }); + + test("extracts name from raw_app path", () => { + expect(extractResourceName("f/my_raw.raw_app/raw_app.yaml", "raw_app")).toBe("f/my_raw"); + }); + + test("returns null when type doesn't match", () => { + expect(extractResourceName("f/script.ts", "flow")).toBeNull(); + }); + + test("works in non-dotted mode", () => { + setNonDottedPaths(true); + expect(extractResourceName("f/my_flow__flow/flow.yaml", "flow")).toBe("f/my_flow"); + }); +}); + +describe("extractFolderPath", () => { + test("extracts folder path from flow", () => { + expect(extractFolderPath("f/my_flow.flow/flow.yaml", "flow")).toBe("f/my_flow.flow/"); + }); + + test("returns null when type doesn't match", () => { + expect(extractFolderPath("f/script.ts", "flow")).toBeNull(); + }); +}); + +describe("buildFolderPath", () => { + test("builds folder path (dotted)", () => { + expect(buildFolderPath("f/my_flow", "flow")).toBe("f/my_flow.flow"); + expect(buildFolderPath("f/dashboard", "app")).toBe("f/dashboard.app"); + expect(buildFolderPath("f/my_raw", "raw_app")).toBe("f/my_raw.raw_app"); + }); + + test("builds folder path (non-dotted)", () => { + setNonDottedPaths(true); + expect(buildFolderPath("f/my_flow", "flow")).toBe("f/my_flow__flow"); + expect(buildFolderPath("f/dashboard", "app")).toBe("f/dashboard__app"); + expect(buildFolderPath("f/my_raw", "raw_app")).toBe("f/my_raw__raw_app"); + }); +}); + +describe("buildMetadataPath", () => { + test("builds metadata path (dotted, yaml)", () => { + expect(buildMetadataPath("f/my_flow", "flow", "yaml")).toBe("f/my_flow.flow/flow.yaml"); + }); + + test("builds metadata path (dotted, json)", () => { + expect(buildMetadataPath("f/dashboard", "app", "json")).toBe("f/dashboard.app/app.json"); + }); + + test("builds metadata path (non-dotted)", () => { + setNonDottedPaths(true); + expect(buildMetadataPath("f/my_flow", "flow", "yaml")).toBe("f/my_flow__flow/flow.yaml"); + }); +}); + +// ============================================================================= +// Folder Validation Functions +// ============================================================================= + +describe("hasFolderSuffix", () => { + test("returns true for matching suffix", () => { + expect(hasFolderSuffix("my_flow.flow", "flow")).toBe(true); + expect(hasFolderSuffix("dashboard.app", "app")).toBe(true); + expect(hasFolderSuffix("my_raw.raw_app", "raw_app")).toBe(true); + }); + + test("returns false for non-matching suffix", () => { + expect(hasFolderSuffix("my_flow.app", "flow")).toBe(false); + expect(hasFolderSuffix("script.ts", "flow")).toBe(false); + }); + + test("works in non-dotted mode", () => { + setNonDottedPaths(true); + expect(hasFolderSuffix("my_flow__flow", "flow")).toBe(true); + expect(hasFolderSuffix("my_flow.flow", "flow")).toBe(false); + }); +}); + +describe("validateFolderName", () => { + test("returns null for valid folder name", () => { + expect(validateFolderName("my_flow.flow", "flow")).toBeNull(); + }); + + test("returns error message for invalid folder name", () => { + const result = validateFolderName("my_flow.app", "flow"); + expect(result).not.toBeNull(); + expect(result).toContain("my_flow.app"); + expect(result).toContain(".flow"); + }); +}); + +describe("extractNameFromFolder", () => { + test("extracts name by removing suffix (dotted)", () => { + expect(extractNameFromFolder("my_flow.flow", "flow")).toBe("my_flow"); + expect(extractNameFromFolder("dashboard.app", "app")).toBe("dashboard"); + expect(extractNameFromFolder("my_raw.raw_app", "raw_app")).toBe("my_raw"); + }); + + test("returns original name if suffix doesn't match", () => { + expect(extractNameFromFolder("my_script", "flow")).toBe("my_script"); + }); + + test("extracts name (non-dotted)", () => { + setNonDottedPaths(true); + expect(extractNameFromFolder("my_flow__flow", "flow")).toBe("my_flow"); + }); +}); + +// ============================================================================= +// Metadata File Detection Functions +// ============================================================================= + +describe("isFlowMetadataFile", () => { + test("detects dotted flow metadata files", () => { + expect(isFlowMetadataFile("f/my_flow.flow.json")).toBe(true); + expect(isFlowMetadataFile("f/my_flow.flow.yaml")).toBe(true); + }); + + test("rejects non-flow metadata files", () => { + expect(isFlowMetadataFile("f/my_app.app.json")).toBe(false); + expect(isFlowMetadataFile("f/script.ts")).toBe(false); + }); + + test("detects non-dotted flow metadata files when configured", () => { + setNonDottedPaths(true); + expect(isFlowMetadataFile("f/my_flow__flow.json")).toBe(true); + expect(isFlowMetadataFile("f/my_flow__flow.yaml")).toBe(true); + // API format (dotted) is always detected + expect(isFlowMetadataFile("f/my_flow.flow.json")).toBe(true); + }); +}); + +describe("isAppMetadataFile", () => { + test("detects dotted app metadata files", () => { + expect(isAppMetadataFile("f/dashboard.app.json")).toBe(true); + expect(isAppMetadataFile("f/dashboard.app.yaml")).toBe(true); + }); + + test("rejects non-app metadata files", () => { + expect(isAppMetadataFile("f/my_flow.flow.json")).toBe(false); + }); + + test("detects non-dotted app metadata files when configured", () => { + setNonDottedPaths(true); + expect(isAppMetadataFile("f/dashboard__app.json")).toBe(true); + // API format always detected + expect(isAppMetadataFile("f/dashboard.app.json")).toBe(true); + }); +}); + +describe("isRawAppMetadataFile", () => { + test("detects dotted raw_app metadata files", () => { + expect(isRawAppMetadataFile("f/my_raw.raw_app.json")).toBe(true); + expect(isRawAppMetadataFile("f/my_raw.raw_app.yaml")).toBe(true); + }); + + test("rejects non-raw_app metadata files", () => { + expect(isRawAppMetadataFile("f/my_app.app.json")).toBe(false); + }); + + test("detects non-dotted raw_app metadata files when configured", () => { + setNonDottedPaths(true); + expect(isRawAppMetadataFile("f/my_raw__raw_app.json")).toBe(true); + expect(isRawAppMetadataFile("f/my_raw.raw_app.json")).toBe(true); + }); +}); + +describe("isRawAppFolderMetadataFile", () => { + test("detects raw_app folder metadata file (dotted)", () => { + expect(isRawAppFolderMetadataFile("f/my_raw.raw_app/raw_app.yaml")).toBe(true); + expect(isRawAppFolderMetadataFile("f/my_raw.raw_app/raw_app.json")).toBe(true); + }); + + test("rejects non-metadata files", () => { + expect(isRawAppFolderMetadataFile("f/my_raw.raw_app/backend/handler.ts")).toBe(false); + }); +}); + +// ============================================================================= +// Sync-related Path Functions +// ============================================================================= + +describe("getDeleteSuffix", () => { + test("returns correct delete suffix", () => { + expect(getDeleteSuffix("flow", "yaml")).toBe(".flow/flow.yaml"); + expect(getDeleteSuffix("app", "json")).toBe(".app/app.json"); + expect(getDeleteSuffix("raw_app", "yaml")).toBe(".raw_app/raw_app.yaml"); + }); + + test("returns correct delete suffix (non-dotted)", () => { + setNonDottedPaths(true); + expect(getDeleteSuffix("flow", "yaml")).toBe("__flow/flow.yaml"); + }); +}); + +describe("transformJsonPathToDir", () => { + test("transforms API dotted .flow.json to dotted dir", () => { + expect(transformJsonPathToDir("f/my_flow.flow.json", "flow")).toBe("f/my_flow.flow"); + }); + + test("transforms API dotted .app.json to dotted dir", () => { + expect(transformJsonPathToDir("f/dashboard.app.json", "app")).toBe("f/dashboard.app"); + }); + + test("transforms API dotted to non-dotted dir when configured", () => { + setNonDottedPaths(true); + expect(transformJsonPathToDir("f/my_flow.flow.json", "flow")).toBe("f/my_flow__flow"); + }); + + test("handles already-configured format", () => { + setNonDottedPaths(true); + expect(transformJsonPathToDir("f/my_flow__flow.json", "flow")).toBe("f/my_flow__flow"); + }); + + test("returns unchanged path when suffix doesn't match", () => { + expect(transformJsonPathToDir("f/script.ts", "flow")).toBe("f/script.ts"); + }); +}); + +// ============================================================================= +// removeWorkerPrefix (from worker-groups.ts) +// ============================================================================= + +describe("removeWorkerPrefix", () => { + test("removes worker__ prefix", () => { + expect(removeWorkerPrefix("worker__default")).toBe("default"); + expect(removeWorkerPrefix("worker__gpu")).toBe("gpu"); + }); + + test("returns name unchanged if no prefix", () => { + expect(removeWorkerPrefix("default")).toBe("default"); + expect(removeWorkerPrefix("gpu")).toBe("gpu"); + }); + + test("handles empty string", () => { + expect(removeWorkerPrefix("")).toBe(""); + }); + + test("handles worker__ as the entire name", () => { + expect(removeWorkerPrefix("worker__")).toBe(""); + }); +}); diff --git a/cli/test/script_envs_sync.test.ts b/cli/test/script_envs_sync.test.ts index 6e0c03bea1..aa515cb5bd 100644 --- a/cli/test/script_envs_sync.test.ts +++ b/cli/test/script_envs_sync.test.ts @@ -7,21 +7,17 @@ * the env variables aren't there anymore. */ -import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; -import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts"; +import { expect, test } from "bun:test"; +import { writeFile, readFile, mkdir } from "node:fs/promises"; import { withTestBackend } from "./test_backend.ts"; -Deno.test({ - name: "Integration: Script envs field is preserved during sync pull/push cycle", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Script envs field is preserved during sync pull/push cycle", async () => { await withTestBackend(async (backend, tempDir) => { const uniqueId = Date.now(); const scriptPath = `f/test/envs_script_${uniqueId}`; // Step 1: Create a script via API with envs set - await ensureDir(`${tempDir}/f/test`); + await mkdir(`${tempDir}/f/test`, { recursive: true }); // Create folder first const folderResp = await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, { @@ -46,102 +42,77 @@ Deno.test({ }), }); - assertEquals( - createResp.ok, - true, - `Failed to create script: ${await createResp.text()}`, - ); + expect(createResp.ok).toEqual(true); // Verify the script was created with envs const getResp = await backend.apiRequest!( `/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`, ); const createdScriptText = await getResp.text(); - assertEquals(getResp.ok, true, `Failed to get script: ${createdScriptText}`); + expect(getResp.ok).toEqual(true); const createdScript = JSON.parse(createdScriptText); - assertEquals( - createdScript.envs, - ["MY_ENV_VAR", "ANOTHER_VAR"], - "Script should have envs after creation", - ); + expect(createdScript.envs).toEqual(["MY_ENV_VAR", "ANOTHER_VAR"]); // Step 2: Create wmill.yaml and sync pull - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "f/test/envs_script_${uniqueId}**" excludes: [] `, + "utf-8", ); const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals( - pullResult.code, - 0, - `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, - ); + expect(pullResult.code).toEqual(0); // Verify the pulled metadata contains envs const metadataPath = `${tempDir}/f/test/envs_script_${uniqueId}.script.yaml`; - const metadataContent = await Deno.readTextFile(metadataPath); - assert( + const metadataContent = await readFile(metadataPath, "utf-8"); + expect( metadataContent.includes("envs:") || metadataContent.includes("MY_ENV_VAR") || metadataContent.includes("ANOTHER_VAR"), - `Pulled metadata should contain envs. Content:\n${metadataContent}`, - ); + ).toBeTruthy(); // Step 3: Modify the script locally (change content) const scriptFilePath = `${tempDir}/f/test/envs_script_${uniqueId}.ts`; - const originalContent = await Deno.readTextFile(scriptFilePath); - await Deno.writeTextFile( + const originalContent = await readFile(scriptFilePath, "utf-8"); + await writeFile( scriptFilePath, originalContent.replace("Hello world", "Hello world modified"), + "utf-8", ); // Step 4: Sync push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); - assertEquals( - pushResult.code, - 0, - `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, - ); + expect(pushResult.code).toEqual(0); // Step 5: Verify envs are still present on the remote const getResp2 = await backend.apiRequest!( `/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`, ); const updatedScriptText = await getResp2.text(); - assertEquals(getResp2.ok, true, `Failed to get script after push: ${updatedScriptText}`); + expect(getResp2.ok).toEqual(true); const updatedScript = JSON.parse(updatedScriptText); - assertEquals( - updatedScript.envs, - ["MY_ENV_VAR", "ANOTHER_VAR"], - `Script envs should be preserved after push. Got: ${JSON.stringify(updatedScript.envs)}`, - ); + expect(updatedScript.envs).toEqual(["MY_ENV_VAR", "ANOTHER_VAR"]); // Also verify the content was updated - assert( + expect( updatedScript.content.includes("Hello world modified"), - "Script content should be updated", - ); + ).toBeTruthy(); }); - }, }); -Deno.test({ - name: "Integration: Script envs field changes are detected and pushed", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Script envs field changes are detected and pushed", async () => { await withTestBackend(async (backend, tempDir) => { const uniqueId = Date.now(); const scriptPath = `f/test/envs_change_${uniqueId}`; // Create folder - await ensureDir(`${tempDir}/f/test`); + await mkdir(`${tempDir}/f/test`, { recursive: true }); await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -162,25 +133,26 @@ Deno.test({ kind: "script", }), }); - assertEquals(createResp.ok, true, `Failed to create script: ${await createResp.text()}`); + expect(createResp.ok).toEqual(true); // Setup wmill.yaml - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "f/test/envs_change_${uniqueId}**" excludes: [] `, + "utf-8", ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pullResult.code, 0, `Pull failed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Modify envs in the local metadata file const metadataPath = `${tempDir}/f/test/envs_change_${uniqueId}.script.yaml`; - let metadataContent = await Deno.readTextFile(metadataPath); + let metadataContent = await readFile(metadataPath, "utf-8"); // Replace the envs line(s) if (metadataContent.includes("envs:")) { @@ -193,40 +165,31 @@ excludes: [] // Add envs if not present metadataContent += "\nenvs:\n - NEW_VAR1\n - NEW_VAR2\n"; } - await Deno.writeTextFile(metadataPath, metadataContent); + await writeFile(metadataPath, metadataContent, "utf-8"); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); - assertEquals(pushResult.code, 0, `Push failed: ${pushResult.stderr}`); + expect(pushResult.code).toEqual(0); // Verify envs were updated on remote const getResp = await backend.apiRequest!( `/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`, ); const scriptText = await getResp.text(); - assertEquals(getResp.ok, true, `Failed to get script: ${scriptText}`); + expect(getResp.ok).toEqual(true); const script = JSON.parse(scriptText); - assertEquals( - script.envs, - ["NEW_VAR1", "NEW_VAR2"], - `Script envs should be updated to new values. Got: ${JSON.stringify(script.envs)}`, - ); + expect(script.envs).toEqual(["NEW_VAR1", "NEW_VAR2"]); }); - }, }); -Deno.test({ - name: "Integration: Script with empty envs is handled correctly", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Script with empty envs is handled correctly", async () => { await withTestBackend(async (backend, tempDir) => { const uniqueId = Date.now(); const scriptPath = `f/test/empty_envs_${uniqueId}`; // Create folder - await ensureDir(`${tempDir}/f/test`); + await mkdir(`${tempDir}/f/test`, { recursive: true }); await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -246,32 +209,34 @@ Deno.test({ kind: "script", }), }); - assertEquals(createResp.ok, true, `Failed to create script: ${await createResp.text()}`); + expect(createResp.ok).toEqual(true); // Setup wmill.yaml - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "f/test/empty_envs_${uniqueId}**" excludes: [] `, + "utf-8", ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pullResult.code, 0, `Pull failed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Modify content const scriptFilePath = `${tempDir}/f/test/empty_envs_${uniqueId}.ts`; - await Deno.writeTextFile( + await writeFile( scriptFilePath, `export async function main() {\n return "Modified no envs";\n}`, + "utf-8", ); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); - assertEquals(pushResult.code, 0, `Push failed: ${pushResult.stderr}`); + expect(pushResult.code).toEqual(0); // Verify script was updated and envs is still null/empty const getResp = await backend.apiRequest!( @@ -279,16 +244,13 @@ excludes: [] ); const script = await getResp.json(); - assert( + expect( script.content.includes("Modified no envs"), - "Script content should be updated", - ); + ).toBeTruthy(); // envs should be null, empty, or undefined - assert( + expect( !script.envs || script.envs.length === 0, - `Script envs should remain empty. Got: ${JSON.stringify(script.envs)}`, - ); + ).toBeTruthy(); }); - }, }); diff --git a/cli/test/settings_unit.test.ts b/cli/test/settings_unit.test.ts new file mode 100644 index 0000000000..2d6a5249ef --- /dev/null +++ b/cli/test/settings_unit.test.ts @@ -0,0 +1,197 @@ +/** + * Unit tests for settings.ts pure functions. + * Tests migrateToGroupedFormat which converts legacy flat settings to grouped format. + */ + +import { expect, test, describe } from "bun:test"; +import { migrateToGroupedFormat } from "../src/core/settings.ts"; + +// ============================================================================= +// migrateToGroupedFormat +// ============================================================================= + +describe("migrateToGroupedFormat", () => { + test("migrates legacy auto_invite fields to grouped format", () => { + const legacy = { + name: "my-workspace", + auto_invite_enabled: true, + auto_invite_as: "operator", + auto_invite_mode: "add", + }; + const result = migrateToGroupedFormat(legacy); + expect(result.auto_invite).toEqual({ + enabled: true, + operator: true, + mode: "add", + }); + }); + + test("migrates legacy auto_invite with non-operator role", () => { + const legacy = { + name: "ws", + auto_invite_enabled: true, + auto_invite_as: "developer", + auto_invite_mode: "invite", + }; + const result = migrateToGroupedFormat(legacy); + expect(result.auto_invite).toEqual({ + enabled: true, + operator: false, + mode: "invite", + }); + }); + + test("migrates legacy auto_invite when disabled", () => { + const legacy = { + name: "ws", + auto_invite_enabled: false, + auto_invite_as: "operator", + }; + const result = migrateToGroupedFormat(legacy); + expect(result.auto_invite!.enabled).toBe(false); + }); + + test("preserves already-grouped auto_invite", () => { + const grouped = { + name: "ws", + auto_invite: { enabled: true, operator: false, mode: "invite" as const }, + }; + const result = migrateToGroupedFormat(grouped); + expect(result.auto_invite).toEqual({ + enabled: true, + operator: false, + mode: "invite", + }); + }); + + test("migrates legacy error_handler string to grouped format", () => { + const legacy = { + name: "ws", + error_handler: "u/admin/error_handler", + error_handler_extra_args: { notify: true }, + error_handler_muted_on_cancel: true, + }; + const result = migrateToGroupedFormat(legacy); + expect(result.error_handler).toEqual({ + path: "u/admin/error_handler", + extra_args: { notify: true }, + muted_on_cancel: true, + }); + }); + + test("preserves already-grouped error_handler", () => { + const grouped = { + name: "ws", + error_handler: { + path: "u/admin/handler", + extra_args: {}, + muted_on_cancel: false, + }, + }; + const result = migrateToGroupedFormat(grouped); + expect(result.error_handler).toEqual({ + path: "u/admin/handler", + extra_args: {}, + muted_on_cancel: false, + }); + }); + + test("migrates legacy success_handler string to grouped format", () => { + const legacy = { + name: "ws", + success_handler: "u/admin/on_success", + success_handler_extra_args: { channel: "#deploys" }, + }; + const result = migrateToGroupedFormat(legacy); + expect(result.success_handler).toEqual({ + path: "u/admin/on_success", + extra_args: { channel: "#deploys" }, + }); + }); + + test("preserves already-grouped success_handler", () => { + const grouped = { + name: "ws", + success_handler: { path: "u/admin/handler", extra_args: {} }, + }; + const result = migrateToGroupedFormat(grouped); + expect(result.success_handler).toEqual({ + path: "u/admin/handler", + extra_args: {}, + }); + }); + + test("copies non-legacy fields through", () => { + const settings = { + name: "my-workspace", + webhook: "https://example.com/hook", + deploy_to: "staging", + default_app: "u/admin/dashboard", + mute_critical_alerts: true, + color: "#ff0000", + }; + const result = migrateToGroupedFormat(settings); + expect(result.name).toBe("my-workspace"); + expect(result.webhook).toBe("https://example.com/hook"); + expect(result.deploy_to).toBe("staging"); + expect(result.default_app).toBe("u/admin/dashboard"); + expect(result.mute_critical_alerts).toBe(true); + expect(result.color).toBe("#ff0000"); + }); + + test("handles minimal settings with only name", () => { + const result = migrateToGroupedFormat({ name: "ws" }); + expect(result.name).toBe("ws"); + expect(result.auto_invite).toBeUndefined(); + expect(result.error_handler).toBeUndefined(); + expect(result.success_handler).toBeUndefined(); + }); + + test("defaults name to empty string when missing", () => { + const result = migrateToGroupedFormat({}); + expect(result.name).toBe(""); + }); + + test("defaults auto_invite_mode to invite when missing", () => { + const legacy = { + name: "ws", + auto_invite_enabled: true, + auto_invite_as: "operator", + }; + const result = migrateToGroupedFormat(legacy); + expect(result.auto_invite!.mode).toBe("invite"); + }); + + test("defaults error_handler_muted_on_cancel to false when missing", () => { + const legacy = { + name: "ws", + error_handler: "u/admin/handler", + }; + const result = migrateToGroupedFormat(legacy); + expect(result.error_handler!.muted_on_cancel).toBe(false); + }); + + test("preserves ai_config, large_file_storage, git_sync, default_scripts, operator_settings", () => { + const settings = { + name: "ws", + ai_config: { provider: "openai" }, + large_file_storage: { type: "s3" }, + git_sync: { enabled: true }, + default_scripts: { python: "template.py" }, + operator_settings: { hideCode: true }, + }; + const result = migrateToGroupedFormat(settings); + expect(result.ai_config).toEqual({ provider: "openai" }); + expect(result.large_file_storage).toEqual({ type: "s3" }); + expect(result.git_sync).toEqual({ enabled: true }); + expect(result.default_scripts).toEqual({ python: "template.py" }); + expect(result.operator_settings).toEqual({ hideCode: true }); + }); + + test("does not include undefined fields in result", () => { + const result = migrateToGroupedFormat({ name: "ws" }); + expect("webhook" in result).toBe(false); + expect("deploy_to" in result).toBe(false); + expect("color" in result).toBe(false); + }); +}); diff --git a/cli/test/setup.ts b/cli/test/setup.ts new file mode 100644 index 0000000000..7eecd8bf2d --- /dev/null +++ b/cli/test/setup.ts @@ -0,0 +1,94 @@ +/** + * Global test setup — preloaded before all test files. + * + * 1. Builds the backend binary so `cargo run` starts instantly. + * 2. Starts a shared backend instance so integration tests don't + * bear the startup cost inside their per-test timeout window. + */ + +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { statSync } from "node:fs"; + +const __dirname = resolve(fileURLToPath(import.meta.url), ".."); + +function findBackendDir(): string { + const candidates = [ + resolve(__dirname, "..", "..", "backend"), + resolve(__dirname, "..", "..", "..", "backend"), + resolve(".", "backend"), + resolve("..", "backend"), + ]; + + for (const candidate of candidates) { + try { + const cargoPath = resolve(candidate, "Cargo.toml"); + const stat = statSync(cargoPath); + if (stat.isFile()) { + return candidate; + } + } catch { + // Continue searching + } + } + + throw new Error("Could not find backend directory."); +} + +// Build the backend binary so `cargo run` is fast for all tests +const backendDir = findBackendDir(); + +const isCI = process.env["CI_MINIMAL_FEATURES"] === "true"; +const hasLicenseKey = !!process.env["EE_LICENSE_KEY"]; +const features = isCI + ? ["zip"] + : hasLicenseKey + ? ["zip", "private", "enterprise", "license"] + : ["zip"]; + +const cargoArgs = ["build", "--features", features.join(",")]; +console.log(`Pre-building backend: cargo ${cargoArgs.join(" ")}`); + +const proc = Bun.spawn(["cargo", ...cargoArgs], { + cwd: backendDir, + stdout: "inherit", + stderr: "inherit", + env: { + ...process.env as Record, + SQLX_OFFLINE: "true", + }, +}); + +const exitCode = await proc.exited; +if (exitCode !== 0) { + throw new Error(`cargo build failed with exit code ${exitCode}`); +} +console.log("Backend build complete."); + +// Start the shared backend instance so it's ready before any test runs. +// This avoids the first integration test timing out while the backend +// creates its database, starts the process, and waits for the health check. +if (process.env["DATABASE_URL"]) { + const { getTestBackend } = await import("./test_backend.ts"); + console.log("Pre-starting test backend..."); + await getTestBackend(); + console.log("Test backend is ready for all tests."); +} + +// When TEST_CLI_RUNTIME=node, also build the npm package so tests +// can invoke `node npm/esm/main.js` instead of `bun run src/main.ts` +if (process.env["TEST_CLI_RUNTIME"] === "node") { + const cliDir = resolve(__dirname, ".."); + console.log("Building npm package for Node runtime testing..."); + const npmBuild = Bun.spawn(["bun", "run", "build-npm.ts"], { + cwd: cliDir, + stdout: "inherit", + stderr: "inherit", + env: process.env as Record, + }); + const npmExit = await npmBuild.exited; + if (npmExit !== 0) { + throw new Error(`npm build failed with exit code ${npmExit}`); + } + console.log("npm package built — tests will use Node runtime."); +} diff --git a/cli/test/specific_items.test.ts b/cli/test/specific_items.test.ts index c3ddaff990..3b72861f88 100644 --- a/cli/test/specific_items.test.ts +++ b/cli/test/specific_items.test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertExists, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; // ============================================================================= // SPECIFIC ITEMS UNIT TESTS @@ -23,192 +23,192 @@ import type { SpecificItemsConfig } from "../src/core/specific_items.ts"; // toBranchSpecificPath TESTS // ============================================================================= -Deno.test("toBranchSpecificPath: converts variable path to branch-specific", () => { +test("toBranchSpecificPath: converts variable path to branch-specific", () => { const result = toBranchSpecificPath("f/test.variable.yaml", "main"); - assertEquals(result, "f/test.main.variable.yaml"); + expect(result).toEqual("f/test.main.variable.yaml"); }); -Deno.test("toBranchSpecificPath: converts resource path to branch-specific", () => { +test("toBranchSpecificPath: converts resource path to branch-specific", () => { const result = toBranchSpecificPath("u/admin/db.resource.yaml", "develop"); - assertEquals(result, "u/admin/db.develop.resource.yaml"); + expect(result).toEqual("u/admin/db.develop.resource.yaml"); }); -Deno.test("toBranchSpecificPath: converts trigger path to branch-specific", () => { +test("toBranchSpecificPath: converts trigger path to branch-specific", () => { const result = toBranchSpecificPath("f/my_trigger.http_trigger.yaml", "feature-x"); - assertEquals(result, "f/my_trigger.feature-x.http_trigger.yaml"); + expect(result).toEqual("f/my_trigger.feature-x.http_trigger.yaml"); }); -Deno.test("toBranchSpecificPath: sanitizes branch names with slashes", () => { +test("toBranchSpecificPath: sanitizes branch names with slashes", () => { const result = toBranchSpecificPath("f/test.variable.yaml", "feature/my-feature"); - assertEquals(result, "f/test.feature_my-feature.variable.yaml"); + expect(result).toEqual("f/test.feature_my-feature.variable.yaml"); }); -Deno.test("toBranchSpecificPath: sanitizes branch names with dots", () => { +test("toBranchSpecificPath: sanitizes branch names with dots", () => { const result = toBranchSpecificPath("f/test.variable.yaml", "release.1.0"); - assertEquals(result, "f/test.release_1_0.variable.yaml"); + expect(result).toEqual("f/test.release_1_0.variable.yaml"); }); -Deno.test("toBranchSpecificPath: leaves non-specific files unchanged", () => { +test("toBranchSpecificPath: leaves non-specific files unchanged", () => { const result = toBranchSpecificPath("f/script.ts", "main"); - assertEquals(result, "f/script.ts"); + expect(result).toEqual("f/script.ts"); }); -Deno.test("toBranchSpecificPath: handles resource files with extensions", () => { +test("toBranchSpecificPath: handles resource files with extensions", () => { const result = toBranchSpecificPath("f/config.resource.file.json", "main"); - assertEquals(result, "f/config.main.resource.file.json"); + expect(result).toEqual("f/config.main.resource.file.json"); }); // ============================================================================= // fromBranchSpecificPath TESTS // ============================================================================= -Deno.test("fromBranchSpecificPath: converts branch-specific variable back to base", () => { +test("fromBranchSpecificPath: converts branch-specific variable back to base", () => { const result = fromBranchSpecificPath("f/test.main.variable.yaml", "main"); - assertEquals(result, "f/test.variable.yaml"); + expect(result).toEqual("f/test.variable.yaml"); }); -Deno.test("fromBranchSpecificPath: converts branch-specific resource back to base", () => { +test("fromBranchSpecificPath: converts branch-specific resource back to base", () => { const result = fromBranchSpecificPath("u/admin/db.develop.resource.yaml", "develop"); - assertEquals(result, "u/admin/db.resource.yaml"); + expect(result).toEqual("u/admin/db.resource.yaml"); }); -Deno.test("fromBranchSpecificPath: converts branch-specific trigger back to base", () => { +test("fromBranchSpecificPath: converts branch-specific trigger back to base", () => { const result = fromBranchSpecificPath("f/my_trigger.feature-x.http_trigger.yaml", "feature-x"); - assertEquals(result, "f/my_trigger.http_trigger.yaml"); + expect(result).toEqual("f/my_trigger.http_trigger.yaml"); }); -Deno.test("fromBranchSpecificPath: handles sanitized branch names", () => { +test("fromBranchSpecificPath: handles sanitized branch names", () => { const result = fromBranchSpecificPath("f/test.feature_my-feature.variable.yaml", "feature/my-feature"); - assertEquals(result, "f/test.variable.yaml"); + expect(result).toEqual("f/test.variable.yaml"); }); -Deno.test("fromBranchSpecificPath: returns unchanged if not branch-specific", () => { +test("fromBranchSpecificPath: returns unchanged if not branch-specific", () => { const result = fromBranchSpecificPath("f/test.variable.yaml", "main"); - assertEquals(result, "f/test.variable.yaml"); + expect(result).toEqual("f/test.variable.yaml"); }); -Deno.test("fromBranchSpecificPath: handles resource files with extensions", () => { +test("fromBranchSpecificPath: handles resource files with extensions", () => { const result = fromBranchSpecificPath("f/config.main.resource.file.json", "main"); - assertEquals(result, "f/config.resource.file.json"); + expect(result).toEqual("f/config.resource.file.json"); }); // ============================================================================= // isSpecificItem TESTS // ============================================================================= -Deno.test("isSpecificItem: returns false when specificItems is undefined", () => { +test("isSpecificItem: returns false when specificItems is undefined", () => { const result = isSpecificItem("f/test.variable.yaml", undefined); - assertEquals(result, false); + expect(result).toEqual(false); }); -Deno.test("isSpecificItem: matches variable paths with glob pattern", () => { +test("isSpecificItem: matches variable paths with glob pattern", () => { const config: SpecificItemsConfig = { variables: ["f/**"], }; - assertEquals(isSpecificItem("f/test.variable.yaml", config), true); - assertEquals(isSpecificItem("u/admin/test.variable.yaml", config), false); + expect(isSpecificItem("f/test.variable.yaml", config)).toEqual(true); + expect(isSpecificItem("u/admin/test.variable.yaml", config)).toEqual(false); }); -Deno.test("isSpecificItem: matches resource paths with glob pattern", () => { +test("isSpecificItem: matches resource paths with glob pattern", () => { const config: SpecificItemsConfig = { resources: ["u/admin/**"], }; - assertEquals(isSpecificItem("u/admin/db.resource.yaml", config), true); - assertEquals(isSpecificItem("f/db.resource.yaml", config), false); + expect(isSpecificItem("u/admin/db.resource.yaml", config)).toEqual(true); + expect(isSpecificItem("f/db.resource.yaml", config)).toEqual(false); }); -Deno.test("isSpecificItem: matches trigger paths with glob pattern", () => { +test("isSpecificItem: matches trigger paths with glob pattern", () => { const config: SpecificItemsConfig = { triggers: ["f/triggers/**"], }; - assertEquals(isSpecificItem("f/triggers/my.http_trigger.yaml", config), true); - assertEquals(isSpecificItem("u/admin/my.http_trigger.yaml", config), false); + expect(isSpecificItem("f/triggers/my.http_trigger.yaml", config)).toEqual(true); + expect(isSpecificItem("u/admin/my.http_trigger.yaml", config)).toEqual(false); }); -Deno.test("isSpecificItem: matches multiple patterns", () => { +test("isSpecificItem: matches multiple patterns", () => { const config: SpecificItemsConfig = { variables: ["f/**", "g/**"], }; - assertEquals(isSpecificItem("f/test.variable.yaml", config), true); - assertEquals(isSpecificItem("g/test.variable.yaml", config), true); - assertEquals(isSpecificItem("u/admin/test.variable.yaml", config), false); + expect(isSpecificItem("f/test.variable.yaml", config)).toEqual(true); + expect(isSpecificItem("g/test.variable.yaml", config)).toEqual(true); + expect(isSpecificItem("u/admin/test.variable.yaml", config)).toEqual(false); }); -Deno.test("isSpecificItem: handles exact path patterns", () => { +test("isSpecificItem: handles exact path patterns", () => { const config: SpecificItemsConfig = { variables: ["f/specific.variable.yaml"], }; - assertEquals(isSpecificItem("f/specific.variable.yaml", config), true); - assertEquals(isSpecificItem("f/other.variable.yaml", config), false); + expect(isSpecificItem("f/specific.variable.yaml", config)).toEqual(true); + expect(isSpecificItem("f/other.variable.yaml", config)).toEqual(false); }); // ============================================================================= // isBranchSpecificFile TESTS // ============================================================================= -Deno.test("isBranchSpecificFile: detects branch-specific variable files", () => { - assertEquals(isBranchSpecificFile("f/test.main.variable.yaml"), true); - assertEquals(isBranchSpecificFile("f/test.develop.variable.yaml"), true); - assertEquals(isBranchSpecificFile("f/test.feature_branch.variable.yaml"), true); +test("isBranchSpecificFile: detects branch-specific variable files", () => { + expect(isBranchSpecificFile("f/test.main.variable.yaml")).toEqual(true); + expect(isBranchSpecificFile("f/test.develop.variable.yaml")).toEqual(true); + expect(isBranchSpecificFile("f/test.feature_branch.variable.yaml")).toEqual(true); }); -Deno.test("isBranchSpecificFile: detects branch-specific resource files", () => { - assertEquals(isBranchSpecificFile("u/admin/db.main.resource.yaml"), true); - assertEquals(isBranchSpecificFile("u/admin/db.staging.resource.yaml"), true); +test("isBranchSpecificFile: detects branch-specific resource files", () => { + expect(isBranchSpecificFile("u/admin/db.main.resource.yaml")).toEqual(true); + expect(isBranchSpecificFile("u/admin/db.staging.resource.yaml")).toEqual(true); }); -Deno.test("isBranchSpecificFile: detects branch-specific trigger files", () => { - assertEquals(isBranchSpecificFile("f/my.main.http_trigger.yaml"), true); - assertEquals(isBranchSpecificFile("f/my.develop.kafka_trigger.yaml"), true); - assertEquals(isBranchSpecificFile("f/my.main.websocket_trigger.yaml"), true); +test("isBranchSpecificFile: detects branch-specific trigger files", () => { + expect(isBranchSpecificFile("f/my.main.http_trigger.yaml")).toEqual(true); + expect(isBranchSpecificFile("f/my.develop.kafka_trigger.yaml")).toEqual(true); + expect(isBranchSpecificFile("f/my.main.websocket_trigger.yaml")).toEqual(true); }); -Deno.test("isBranchSpecificFile: returns false for non-branch-specific files", () => { - assertEquals(isBranchSpecificFile("f/test.variable.yaml"), false); - assertEquals(isBranchSpecificFile("u/admin/db.resource.yaml"), false); - assertEquals(isBranchSpecificFile("f/my.http_trigger.yaml"), false); - assertEquals(isBranchSpecificFile("f/script.ts"), false); +test("isBranchSpecificFile: returns false for non-branch-specific files", () => { + expect(isBranchSpecificFile("f/test.variable.yaml")).toEqual(false); + expect(isBranchSpecificFile("u/admin/db.resource.yaml")).toEqual(false); + expect(isBranchSpecificFile("f/my.http_trigger.yaml")).toEqual(false); + expect(isBranchSpecificFile("f/script.ts")).toEqual(false); }); -Deno.test("isBranchSpecificFile: handles resource files with extensions", () => { - assertEquals(isBranchSpecificFile("f/config.main.resource.file.json"), true); - assertEquals(isBranchSpecificFile("f/config.resource.file.json"), false); +test("isBranchSpecificFile: handles resource files with extensions", () => { + expect(isBranchSpecificFile("f/config.main.resource.file.json")).toEqual(true); + expect(isBranchSpecificFile("f/config.resource.file.json")).toEqual(false); }); // ============================================================================= // ROUND-TRIP TESTS // ============================================================================= -Deno.test("round-trip: variable file path conversion", () => { +test("round-trip: variable file path conversion", () => { const original = "f/my/nested/config.variable.yaml"; const branch = "feature/test-branch"; const branchSpecific = toBranchSpecificPath(original, branch); const restored = fromBranchSpecificPath(branchSpecific, branch); - assertEquals(restored, original); + expect(restored).toEqual(original); }); -Deno.test("round-trip: resource file path conversion", () => { +test("round-trip: resource file path conversion", () => { const original = "u/admin/database.resource.yaml"; const branch = "develop"; const branchSpecific = toBranchSpecificPath(original, branch); const restored = fromBranchSpecificPath(branchSpecific, branch); - assertEquals(restored, original); + expect(restored).toEqual(original); }); -Deno.test("round-trip: trigger file path conversion", () => { +test("round-trip: trigger file path conversion", () => { const original = "f/webhooks/handler.http_trigger.yaml"; const branch = "main"; const branchSpecific = toBranchSpecificPath(original, branch); const restored = fromBranchSpecificPath(branchSpecific, branch); - assertEquals(restored, original); + expect(restored).toEqual(original); }); -Deno.test("round-trip: resource file with extension", () => { +test("round-trip: resource file with extension", () => { const original = "f/configs/settings.resource.file.ini"; const branch = "release/v1.0"; const branchSpecific = toBranchSpecificPath(original, branch); const restored = fromBranchSpecificPath(branchSpecific, branch); - assertEquals(restored, original); + expect(restored).toEqual(original); }); // ============================================================================= @@ -216,7 +216,7 @@ Deno.test("round-trip: resource file with extension", () => { // These tests validate that functions work correctly with explicit branch override // ============================================================================= -Deno.test("branchOverride: getBranchSpecificPath with override returns branch-specific path", () => { +test("branchOverride: getBranchSpecificPath with override returns branch-specific path", () => { // This test verifies that when branchOverride is provided, the function uses it // instead of detecting the current git branch const config: SpecificItemsConfig = { @@ -225,10 +225,10 @@ Deno.test("branchOverride: getBranchSpecificPath with override returns branch-sp // When override is provided, it should return the branch-specific path even outside git repo const result = getBranchSpecificPath("f/test.variable.yaml", config, "staging"); - assertEquals(result, "f/test.staging.variable.yaml"); + expect(result).toEqual("f/test.staging.variable.yaml"); }); -Deno.test("branchOverride: getBranchSpecificPath without override and not in git repo returns undefined", () => { +test("branchOverride: getBranchSpecificPath without override and not in git repo returns undefined", () => { const config: SpecificItemsConfig = { variables: ["f/**"], }; @@ -240,31 +240,31 @@ Deno.test("branchOverride: getBranchSpecificPath without override and not in git // We test the override case above which is deterministic }); -Deno.test("branchOverride: isCurrentBranchFile with override uses provided branch", () => { +test("branchOverride: isCurrentBranchFile with override uses provided branch", () => { // Test that isCurrentBranchFile uses the override branch instead of git detection const result = isCurrentBranchFile("f/test.staging.variable.yaml", "staging"); - assertEquals(result, true); + expect(result).toEqual(true); // Should return false for different branch const resultOther = isCurrentBranchFile("f/test.staging.variable.yaml", "production"); - assertEquals(resultOther, false); + expect(resultOther).toEqual(false); // Should return false for non-branch-specific file const resultNonSpecific = isCurrentBranchFile("f/test.variable.yaml", "staging"); - assertEquals(resultNonSpecific, false); + expect(resultNonSpecific).toEqual(false); }); -Deno.test("branchOverride: isCurrentBranchFile with override handles sanitized branch names", () => { +test("branchOverride: isCurrentBranchFile with override handles sanitized branch names", () => { // Test with branch names that get sanitized const result = isCurrentBranchFile("f/test.feature_my-branch.variable.yaml", "feature/my-branch"); - assertEquals(result, true); + expect(result).toEqual(true); // Different sanitized branch should return false const resultOther = isCurrentBranchFile("f/test.feature_my-branch.variable.yaml", "feature/other-branch"); - assertEquals(resultOther, false); + expect(resultOther).toEqual(false); }); -Deno.test("branchOverride: getSpecificItemsForCurrentBranch with override returns correct config", () => { +test("branchOverride: getSpecificItemsForCurrentBranch with override returns correct config", () => { // Test that getSpecificItemsForCurrentBranch uses the override branch const config = { gitBranches: { @@ -286,17 +286,17 @@ Deno.test("branchOverride: getSpecificItemsForCurrentBranch with override return }; const stagingItems = getSpecificItemsForCurrentBranch(config as any, "staging"); - assertEquals(stagingItems?.variables, ["f/**"]); - assertEquals(stagingItems?.resources, ["u/admin/**"]); - assertEquals(stagingItems?.triggers, ["f/webhooks/**"]); // From common + expect(stagingItems?.variables).toEqual(["f/**"]); + expect(stagingItems?.resources).toEqual(["u/admin/**"]); + expect(stagingItems?.triggers).toEqual(["f/webhooks/**"]); // From common const productionItems = getSpecificItemsForCurrentBranch(config as any, "production"); - assertEquals(productionItems?.variables, ["g/**"]); - assertEquals(productionItems?.resources, undefined); - assertEquals(productionItems?.triggers, ["f/webhooks/**"]); // From common + expect(productionItems?.variables).toEqual(["g/**"]); + expect(productionItems?.resources).toEqual(undefined); + expect(productionItems?.triggers).toEqual(["f/webhooks/**"]); // From common }); -Deno.test("branchOverride: getSpecificItemsForCurrentBranch with non-existent branch returns undefined", () => { +test("branchOverride: getSpecificItemsForCurrentBranch with non-existent branch returns undefined", () => { const config = { gitBranches: { staging: { @@ -309,10 +309,10 @@ Deno.test("branchOverride: getSpecificItemsForCurrentBranch with non-existent br // When the branch doesn't have specific items (and there's no common), should return undefined const result = getSpecificItemsForCurrentBranch(config as any, "nonexistent"); - assertEquals(result, undefined); + expect(result).toEqual(undefined); }); -Deno.test("branchOverride: getSpecificItemsForCurrentBranch merges common and branch items", () => { +test("branchOverride: getSpecificItemsForCurrentBranch merges common and branch items", () => { const config = { gitBranches: { commonSpecificItems: { @@ -330,9 +330,9 @@ Deno.test("branchOverride: getSpecificItemsForCurrentBranch merges common and br const result = getSpecificItemsForCurrentBranch(config as any, "develop"); // Should merge common and branch-specific - assertEquals(result?.variables, ["common/**", "dev/**"]); - assertEquals(result?.resources, ["shared/**"]); - assertEquals(result?.triggers, ["dev/triggers/**"]); + expect(result?.variables).toEqual(["common/**", "dev/**"]); + expect(result?.resources).toEqual(["shared/**"]); + expect(result?.triggers).toEqual(["dev/triggers/**"]); }); // ============================================================================= @@ -340,176 +340,176 @@ Deno.test("branchOverride: getSpecificItemsForCurrentBranch merges common and br // Format: f/folder/folder.branchName.meta.yaml // ============================================================================= -Deno.test("toBranchSpecificPath: converts folder meta path to branch-specific", () => { +test("toBranchSpecificPath: converts folder meta path to branch-specific", () => { // f/my_folder/folder.meta.yaml -> f/my_folder/folder.main.meta.yaml const result = toBranchSpecificPath("f/my_folder/folder.meta.yaml", "main"); - assertEquals(result, "f/my_folder/folder.main.meta.yaml"); + expect(result).toEqual("f/my_folder/folder.main.meta.yaml"); }); -Deno.test("toBranchSpecificPath: converts nested folder meta path to branch-specific", () => { +test("toBranchSpecificPath: converts nested folder meta path to branch-specific", () => { const result = toBranchSpecificPath("f/parent/child/folder.meta.yaml", "develop"); - assertEquals(result, "f/parent/child/folder.develop.meta.yaml"); + expect(result).toEqual("f/parent/child/folder.develop.meta.yaml"); }); -Deno.test("toBranchSpecificPath: sanitizes branch name in folder path", () => { +test("toBranchSpecificPath: sanitizes branch name in folder path", () => { const result = toBranchSpecificPath("f/env/folder.meta.yaml", "feature/test"); - assertEquals(result, "f/env/folder.feature_test.meta.yaml"); + expect(result).toEqual("f/env/folder.feature_test.meta.yaml"); }); -Deno.test("fromBranchSpecificPath: converts branch-specific folder back to base", () => { +test("fromBranchSpecificPath: converts branch-specific folder back to base", () => { const result = fromBranchSpecificPath("f/my_folder/folder.main.meta.yaml", "main"); - assertEquals(result, "f/my_folder/folder.meta.yaml"); + expect(result).toEqual("f/my_folder/folder.meta.yaml"); }); -Deno.test("fromBranchSpecificPath: handles nested branch-specific folder", () => { +test("fromBranchSpecificPath: handles nested branch-specific folder", () => { const result = fromBranchSpecificPath("f/parent/child/folder.develop.meta.yaml", "develop"); - assertEquals(result, "f/parent/child/folder.meta.yaml"); + expect(result).toEqual("f/parent/child/folder.meta.yaml"); }); -Deno.test("fromBranchSpecificPath: handles sanitized branch names for folders", () => { +test("fromBranchSpecificPath: handles sanitized branch names for folders", () => { const result = fromBranchSpecificPath("f/env/folder.feature_test.meta.yaml", "feature/test"); - assertEquals(result, "f/env/folder.meta.yaml"); + expect(result).toEqual("f/env/folder.meta.yaml"); }); -Deno.test("isSpecificItem: matches folder paths with glob pattern", () => { +test("isSpecificItem: matches folder paths with glob pattern", () => { const config: SpecificItemsConfig = { folders: ["f/env_*"], }; - assertEquals(isSpecificItem("f/env_staging/folder.meta.yaml", config), true); - assertEquals(isSpecificItem("f/env_production/folder.meta.yaml", config), true); - assertEquals(isSpecificItem("f/other/folder.meta.yaml", config), false); + expect(isSpecificItem("f/env_staging/folder.meta.yaml", config)).toEqual(true); + expect(isSpecificItem("f/env_production/folder.meta.yaml", config)).toEqual(true); + expect(isSpecificItem("f/other/folder.meta.yaml", config)).toEqual(false); }); -Deno.test("isSpecificItem: matches folder paths with exact pattern", () => { +test("isSpecificItem: matches folder paths with exact pattern", () => { const config: SpecificItemsConfig = { folders: ["f/config"], }; - assertEquals(isSpecificItem("f/config/folder.meta.yaml", config), true); - assertEquals(isSpecificItem("f/other/folder.meta.yaml", config), false); + expect(isSpecificItem("f/config/folder.meta.yaml", config)).toEqual(true); + expect(isSpecificItem("f/other/folder.meta.yaml", config)).toEqual(false); }); -Deno.test("isBranchSpecificFile: detects branch-specific folder files", () => { - assertEquals(isBranchSpecificFile("f/my_folder/folder.main.meta.yaml"), true); - assertEquals(isBranchSpecificFile("f/my_folder/folder.develop.meta.yaml"), true); - assertEquals(isBranchSpecificFile("f/nested/path/folder.staging.meta.yaml"), true); +test("isBranchSpecificFile: detects branch-specific folder files", () => { + expect(isBranchSpecificFile("f/my_folder/folder.main.meta.yaml")).toEqual(true); + expect(isBranchSpecificFile("f/my_folder/folder.develop.meta.yaml")).toEqual(true); + expect(isBranchSpecificFile("f/nested/path/folder.staging.meta.yaml")).toEqual(true); }); -Deno.test("isBranchSpecificFile: returns false for non-branch-specific folder files", () => { - assertEquals(isBranchSpecificFile("f/my_folder/folder.meta.yaml"), false); - assertEquals(isBranchSpecificFile("f/nested/path/folder.meta.yaml"), false); +test("isBranchSpecificFile: returns false for non-branch-specific folder files", () => { + expect(isBranchSpecificFile("f/my_folder/folder.meta.yaml")).toEqual(false); + expect(isBranchSpecificFile("f/nested/path/folder.meta.yaml")).toEqual(false); }); -Deno.test("isCurrentBranchFile: detects branch-specific folder for current branch", () => { - assertEquals(isCurrentBranchFile("f/my_folder/folder.staging.meta.yaml", "staging"), true); - assertEquals(isCurrentBranchFile("f/my_folder/folder.staging.meta.yaml", "production"), false); - assertEquals(isCurrentBranchFile("f/my_folder/folder.meta.yaml", "staging"), false); +test("isCurrentBranchFile: detects branch-specific folder for current branch", () => { + expect(isCurrentBranchFile("f/my_folder/folder.staging.meta.yaml", "staging")).toEqual(true); + expect(isCurrentBranchFile("f/my_folder/folder.staging.meta.yaml", "production")).toEqual(false); + expect(isCurrentBranchFile("f/my_folder/folder.meta.yaml", "staging")).toEqual(false); }); -Deno.test("isCurrentBranchFile: handles sanitized branch for folders", () => { - assertEquals(isCurrentBranchFile("f/env/folder.feature_test.meta.yaml", "feature/test"), true); - assertEquals(isCurrentBranchFile("f/env/folder.feature_test.meta.yaml", "feature/other"), false); +test("isCurrentBranchFile: handles sanitized branch for folders", () => { + expect(isCurrentBranchFile("f/env/folder.feature_test.meta.yaml", "feature/test")).toEqual(true); + expect(isCurrentBranchFile("f/env/folder.feature_test.meta.yaml", "feature/other")).toEqual(false); }); -Deno.test("round-trip: folder meta path conversion", () => { +test("round-trip: folder meta path conversion", () => { const original = "f/configs/env_folder/folder.meta.yaml"; const branch = "main"; const branchSpecific = toBranchSpecificPath(original, branch); - assertEquals(branchSpecific, "f/configs/env_folder/folder.main.meta.yaml"); + expect(branchSpecific).toEqual("f/configs/env_folder/folder.main.meta.yaml"); const restored = fromBranchSpecificPath(branchSpecific, branch); - assertEquals(restored, original); + expect(restored).toEqual(original); }); -Deno.test("round-trip: folder meta with sanitized branch", () => { +test("round-trip: folder meta with sanitized branch", () => { const original = "f/env/folder.meta.yaml"; const branch = "feature/new-env"; const branchSpecific = toBranchSpecificPath(original, branch); - assertEquals(branchSpecific, "f/env/folder.feature_new-env.meta.yaml"); + expect(branchSpecific).toEqual("f/env/folder.feature_new-env.meta.yaml"); const restored = fromBranchSpecificPath(branchSpecific, branch); - assertEquals(restored, original); + expect(restored).toEqual(original); }); // ============================================================================= // SETTINGS BRANCH-SPECIFIC TESTS // ============================================================================= -Deno.test("toBranchSpecificPath: converts settings.yaml to branch-specific", () => { +test("toBranchSpecificPath: converts settings.yaml to branch-specific", () => { const result = toBranchSpecificPath("settings.yaml", "main"); - assertEquals(result, "settings.main.yaml"); + expect(result).toEqual("settings.main.yaml"); }); -Deno.test("toBranchSpecificPath: sanitizes branch name in settings path", () => { +test("toBranchSpecificPath: sanitizes branch name in settings path", () => { const result = toBranchSpecificPath("settings.yaml", "feature/test"); - assertEquals(result, "settings.feature_test.yaml"); + expect(result).toEqual("settings.feature_test.yaml"); }); -Deno.test("fromBranchSpecificPath: converts branch-specific settings back to base", () => { +test("fromBranchSpecificPath: converts branch-specific settings back to base", () => { const result = fromBranchSpecificPath("settings.main.yaml", "main"); - assertEquals(result, "settings.yaml"); + expect(result).toEqual("settings.yaml"); }); -Deno.test("fromBranchSpecificPath: handles sanitized branch names for settings", () => { +test("fromBranchSpecificPath: handles sanitized branch names for settings", () => { const result = fromBranchSpecificPath("settings.feature_test.yaml", "feature/test"); - assertEquals(result, "settings.yaml"); + expect(result).toEqual("settings.yaml"); }); -Deno.test("isSpecificItem: matches settings.yaml when settings is true", () => { +test("isSpecificItem: matches settings.yaml when settings is true", () => { const config: SpecificItemsConfig = { settings: true, }; - assertEquals(isSpecificItem("settings.yaml", config), true); + expect(isSpecificItem("settings.yaml", config)).toEqual(true); }); -Deno.test("isSpecificItem: does not match settings.yaml when settings is false", () => { +test("isSpecificItem: does not match settings.yaml when settings is false", () => { const config: SpecificItemsConfig = { settings: false, }; - assertEquals(isSpecificItem("settings.yaml", config), false); + expect(isSpecificItem("settings.yaml", config)).toEqual(false); }); -Deno.test("isSpecificItem: does not match settings.yaml when settings is undefined", () => { +test("isSpecificItem: does not match settings.yaml when settings is undefined", () => { const config: SpecificItemsConfig = { variables: ["f/**"], }; - assertEquals(isSpecificItem("settings.yaml", config), false); + expect(isSpecificItem("settings.yaml", config)).toEqual(false); }); -Deno.test("isBranchSpecificFile: detects branch-specific settings files", () => { - assertEquals(isBranchSpecificFile("settings.main.yaml"), true); - assertEquals(isBranchSpecificFile("settings.develop.yaml"), true); - assertEquals(isBranchSpecificFile("settings.feature_test.yaml"), true); +test("isBranchSpecificFile: detects branch-specific settings files", () => { + expect(isBranchSpecificFile("settings.main.yaml")).toEqual(true); + expect(isBranchSpecificFile("settings.develop.yaml")).toEqual(true); + expect(isBranchSpecificFile("settings.feature_test.yaml")).toEqual(true); }); -Deno.test("isBranchSpecificFile: returns false for non-branch-specific settings", () => { - assertEquals(isBranchSpecificFile("settings.yaml"), false); +test("isBranchSpecificFile: returns false for non-branch-specific settings", () => { + expect(isBranchSpecificFile("settings.yaml")).toEqual(false); }); -Deno.test("isCurrentBranchFile: detects branch-specific settings for current branch", () => { - assertEquals(isCurrentBranchFile("settings.staging.yaml", "staging"), true); - assertEquals(isCurrentBranchFile("settings.staging.yaml", "production"), false); - assertEquals(isCurrentBranchFile("settings.yaml", "staging"), false); +test("isCurrentBranchFile: detects branch-specific settings for current branch", () => { + expect(isCurrentBranchFile("settings.staging.yaml", "staging")).toEqual(true); + expect(isCurrentBranchFile("settings.staging.yaml", "production")).toEqual(false); + expect(isCurrentBranchFile("settings.yaml", "staging")).toEqual(false); }); -Deno.test("isCurrentBranchFile: handles sanitized branch for settings", () => { - assertEquals(isCurrentBranchFile("settings.feature_test.yaml", "feature/test"), true); - assertEquals(isCurrentBranchFile("settings.feature_test.yaml", "feature/other"), false); +test("isCurrentBranchFile: handles sanitized branch for settings", () => { + expect(isCurrentBranchFile("settings.feature_test.yaml", "feature/test")).toEqual(true); + expect(isCurrentBranchFile("settings.feature_test.yaml", "feature/other")).toEqual(false); }); -Deno.test("round-trip: settings path conversion", () => { +test("round-trip: settings path conversion", () => { const original = "settings.yaml"; const branch = "main"; const branchSpecific = toBranchSpecificPath(original, branch); - assertEquals(branchSpecific, "settings.main.yaml"); + expect(branchSpecific).toEqual("settings.main.yaml"); const restored = fromBranchSpecificPath(branchSpecific, branch); - assertEquals(restored, original); + expect(restored).toEqual(original); }); -Deno.test("round-trip: settings with sanitized branch", () => { +test("round-trip: settings with sanitized branch", () => { const original = "settings.yaml"; const branch = "release/v1.0"; const branchSpecific = toBranchSpecificPath(original, branch); - assertEquals(branchSpecific, "settings.release_v1_0.yaml"); + expect(branchSpecific).toEqual("settings.release_v1_0.yaml"); const restored = fromBranchSpecificPath(branchSpecific, branch); - assertEquals(restored, original); + expect(restored).toEqual(original); }); // ============================================================================= @@ -518,111 +518,111 @@ Deno.test("round-trip: settings with sanitized branch", () => { // Used to determine if branch-specific files should be used for this type. // ============================================================================= -Deno.test("isItemTypeConfigured: returns false when specificItems is undefined", () => { - assertEquals(isItemTypeConfigured("f/test.variable.yaml", undefined), false); - assertEquals(isItemTypeConfigured("f/test.resource.yaml", undefined), false); - assertEquals(isItemTypeConfigured("f/folder/folder.meta.yaml", undefined), false); - assertEquals(isItemTypeConfigured("settings.yaml", undefined), false); +test("isItemTypeConfigured: returns false when specificItems is undefined", () => { + expect(isItemTypeConfigured("f/test.variable.yaml", undefined)).toEqual(false); + expect(isItemTypeConfigured("f/test.resource.yaml", undefined)).toEqual(false); + expect(isItemTypeConfigured("f/folder/folder.meta.yaml", undefined)).toEqual(false); + expect(isItemTypeConfigured("settings.yaml", undefined)).toEqual(false); }); -Deno.test("isItemTypeConfigured: returns true for variables when variables is configured", () => { +test("isItemTypeConfigured: returns true for variables when variables is configured", () => { const config: SpecificItemsConfig = { variables: ["f/**"], }; // Type is configured (even if path doesn't match the pattern) - assertEquals(isItemTypeConfigured("f/test.variable.yaml", config), true); - assertEquals(isItemTypeConfigured("g/other.variable.yaml", config), true); + expect(isItemTypeConfigured("f/test.variable.yaml", config)).toEqual(true); + expect(isItemTypeConfigured("g/other.variable.yaml", config)).toEqual(true); }); -Deno.test("isItemTypeConfigured: returns false for variables when variables is NOT configured", () => { +test("isItemTypeConfigured: returns false for variables when variables is NOT configured", () => { const config: SpecificItemsConfig = { resources: ["f/**"], }; - assertEquals(isItemTypeConfigured("f/test.variable.yaml", config), false); + expect(isItemTypeConfigured("f/test.variable.yaml", config)).toEqual(false); }); -Deno.test("isItemTypeConfigured: returns true for resources when resources is configured", () => { +test("isItemTypeConfigured: returns true for resources when resources is configured", () => { const config: SpecificItemsConfig = { resources: ["f/**"], }; - assertEquals(isItemTypeConfigured("f/test.resource.yaml", config), true); - assertEquals(isItemTypeConfigured("g/other.resource.yaml", config), true); + expect(isItemTypeConfigured("f/test.resource.yaml", config)).toEqual(true); + expect(isItemTypeConfigured("g/other.resource.yaml", config)).toEqual(true); }); -Deno.test("isItemTypeConfigured: returns false for resources when resources is NOT configured", () => { +test("isItemTypeConfigured: returns false for resources when resources is NOT configured", () => { const config: SpecificItemsConfig = { variables: ["f/**"], }; - assertEquals(isItemTypeConfigured("f/test.resource.yaml", config), false); + expect(isItemTypeConfigured("f/test.resource.yaml", config)).toEqual(false); }); -Deno.test("isItemTypeConfigured: returns true for triggers when triggers is configured", () => { +test("isItemTypeConfigured: returns true for triggers when triggers is configured", () => { const config: SpecificItemsConfig = { triggers: ["f/**"], }; - assertEquals(isItemTypeConfigured("f/my.http_trigger.yaml", config), true); - assertEquals(isItemTypeConfigured("f/my.kafka_trigger.yaml", config), true); - assertEquals(isItemTypeConfigured("g/other.websocket_trigger.yaml", config), true); + expect(isItemTypeConfigured("f/my.http_trigger.yaml", config)).toEqual(true); + expect(isItemTypeConfigured("f/my.kafka_trigger.yaml", config)).toEqual(true); + expect(isItemTypeConfigured("g/other.websocket_trigger.yaml", config)).toEqual(true); }); -Deno.test("isItemTypeConfigured: returns false for triggers when triggers is NOT configured", () => { +test("isItemTypeConfigured: returns false for triggers when triggers is NOT configured", () => { const config: SpecificItemsConfig = { variables: ["f/**"], }; - assertEquals(isItemTypeConfigured("f/my.http_trigger.yaml", config), false); + expect(isItemTypeConfigured("f/my.http_trigger.yaml", config)).toEqual(false); }); -Deno.test("isItemTypeConfigured: returns true for folders when folders is configured", () => { +test("isItemTypeConfigured: returns true for folders when folders is configured", () => { const config: SpecificItemsConfig = { folders: ["f/env_*"], }; // Type is configured (even if path doesn't match the pattern) - assertEquals(isItemTypeConfigured("f/env_staging/folder.meta.yaml", config), true); - assertEquals(isItemTypeConfigured("f/other/folder.meta.yaml", config), true); + expect(isItemTypeConfigured("f/env_staging/folder.meta.yaml", config)).toEqual(true); + expect(isItemTypeConfigured("f/other/folder.meta.yaml", config)).toEqual(true); }); -Deno.test("isItemTypeConfigured: returns false for folders when folders is NOT configured", () => { +test("isItemTypeConfigured: returns false for folders when folders is NOT configured", () => { const config: SpecificItemsConfig = { variables: ["f/**"], }; - assertEquals(isItemTypeConfigured("f/my_folder/folder.meta.yaml", config), false); + expect(isItemTypeConfigured("f/my_folder/folder.meta.yaml", config)).toEqual(false); }); -Deno.test("isItemTypeConfigured: returns true for settings when settings is configured (true)", () => { +test("isItemTypeConfigured: returns true for settings when settings is configured (true)", () => { const config: SpecificItemsConfig = { settings: true, }; - assertEquals(isItemTypeConfigured("settings.yaml", config), true); + expect(isItemTypeConfigured("settings.yaml", config)).toEqual(true); }); -Deno.test("isItemTypeConfigured: returns true for settings when settings is configured (false)", () => { +test("isItemTypeConfigured: returns true for settings when settings is configured (false)", () => { // settings: false still means the type is "configured" (explicitly disabled) const config: SpecificItemsConfig = { settings: false, }; - assertEquals(isItemTypeConfigured("settings.yaml", config), true); + expect(isItemTypeConfigured("settings.yaml", config)).toEqual(true); }); -Deno.test("isItemTypeConfigured: returns false for settings when settings is NOT configured", () => { +test("isItemTypeConfigured: returns false for settings when settings is NOT configured", () => { const config: SpecificItemsConfig = { variables: ["f/**"], }; - assertEquals(isItemTypeConfigured("settings.yaml", config), false); + expect(isItemTypeConfigured("settings.yaml", config)).toEqual(false); }); -Deno.test("isItemTypeConfigured: returns true for resource files (with extension) when resources is configured", () => { +test("isItemTypeConfigured: returns true for resource files (with extension) when resources is configured", () => { const config: SpecificItemsConfig = { resources: ["f/**"], }; - assertEquals(isItemTypeConfigured("f/config.resource.file.json", config), true); - assertEquals(isItemTypeConfigured("f/data.resource.file.ini", config), true); + expect(isItemTypeConfigured("f/config.resource.file.json", config)).toEqual(true); + expect(isItemTypeConfigured("f/data.resource.file.ini", config)).toEqual(true); }); -Deno.test("isItemTypeConfigured: returns false for resource files when resources is NOT configured", () => { +test("isItemTypeConfigured: returns false for resource files when resources is NOT configured", () => { const config: SpecificItemsConfig = { variables: ["f/**"], }; - assertEquals(isItemTypeConfigured("f/config.resource.file.json", config), false); + expect(isItemTypeConfigured("f/config.resource.file.json", config)).toEqual(false); }); // ============================================================================= @@ -632,7 +632,7 @@ Deno.test("isItemTypeConfigured: returns false for resource files when resources // - When type is NOT configured: skip branch-specific files, use base files // ============================================================================= -Deno.test("filtering logic: folders - when NOT configured, branch-specific should be ignored", () => { +test("filtering logic: folders - when NOT configured, branch-specific should be ignored", () => { // Config has variables but NOT folders const config: SpecificItemsConfig = { variables: ["f/**"], @@ -642,17 +642,17 @@ Deno.test("filtering logic: folders - when NOT configured, branch-specific shoul const branchSpecificPath = "f/my_folder/folder.main.meta.yaml"; // Folder type is NOT configured - assertEquals(isItemTypeConfigured(basePath, config), false); + expect(isItemTypeConfigured(basePath, config)).toEqual(false); // Therefore, branch-specific file detection should not apply to this type // The sync logic should: // 1. Skip branch-specific folder files (isBranchSpecificFile returns true) // 2. Use the base file - assertEquals(isBranchSpecificFile(branchSpecificPath), true); - assertEquals(isBranchSpecificFile(basePath), false); + expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true); + expect(isBranchSpecificFile(basePath)).toEqual(false); }); -Deno.test("filtering logic: folders - when IS configured and matches, use branch-specific", () => { +test("filtering logic: folders - when IS configured and matches, use branch-specific", () => { const config: SpecificItemsConfig = { folders: ["f/my_folder"], }; @@ -661,19 +661,19 @@ Deno.test("filtering logic: folders - when IS configured and matches, use branch const branchSpecificPath = "f/my_folder/folder.main.meta.yaml"; // Folder type IS configured - assertEquals(isItemTypeConfigured(basePath, config), true); + expect(isItemTypeConfigured(basePath, config)).toEqual(true); // And path matches the pattern - assertEquals(isSpecificItem(basePath, config), true); + expect(isSpecificItem(basePath, config)).toEqual(true); // The sync logic should: // 1. Use branch-specific folder file (map to base path) // 2. Skip the base file - assertEquals(isBranchSpecificFile(branchSpecificPath), true); - assertEquals(fromBranchSpecificPath(branchSpecificPath, "main"), basePath); + expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true); + expect(fromBranchSpecificPath(branchSpecificPath, "main")).toEqual(basePath); }); -Deno.test("filtering logic: folders - when IS configured but doesn't match, skip branch-specific", () => { +test("filtering logic: folders - when IS configured but doesn't match, skip branch-specific", () => { const config: SpecificItemsConfig = { folders: ["f/env_*"], // Only env_ folders are branch-specific }; @@ -682,17 +682,17 @@ Deno.test("filtering logic: folders - when IS configured but doesn't match, skip const branchSpecificPath = "f/other_folder/folder.main.meta.yaml"; // Folder type IS configured - assertEquals(isItemTypeConfigured(basePath, config), true); + expect(isItemTypeConfigured(basePath, config)).toEqual(true); // But this path doesn't match the pattern - assertEquals(isSpecificItem(basePath, config), false); + expect(isSpecificItem(basePath, config)).toEqual(false); // The sync logic should: // 1. Skip the branch-specific file (type configured but doesn't match) // 2. Use the base file }); -Deno.test("filtering logic: settings - when NOT configured, branch-specific should be ignored", () => { +test("filtering logic: settings - when NOT configured, branch-specific should be ignored", () => { // Config has variables but NOT settings const config: SpecificItemsConfig = { variables: ["f/**"], @@ -702,14 +702,14 @@ Deno.test("filtering logic: settings - when NOT configured, branch-specific shou const branchSpecificPath = "settings.main.yaml"; // Settings type is NOT configured - assertEquals(isItemTypeConfigured(basePath, config), false); + expect(isItemTypeConfigured(basePath, config)).toEqual(false); // Therefore, branch-specific file detection should not apply to this type - assertEquals(isBranchSpecificFile(branchSpecificPath), true); - assertEquals(isBranchSpecificFile(basePath), false); + expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true); + expect(isBranchSpecificFile(basePath)).toEqual(false); }); -Deno.test("filtering logic: settings - when IS configured (true), use branch-specific", () => { +test("filtering logic: settings - when IS configured (true), use branch-specific", () => { const config: SpecificItemsConfig = { settings: true, }; @@ -718,17 +718,17 @@ Deno.test("filtering logic: settings - when IS configured (true), use branch-spe const branchSpecificPath = "settings.main.yaml"; // Settings type IS configured - assertEquals(isItemTypeConfigured(basePath, config), true); + expect(isItemTypeConfigured(basePath, config)).toEqual(true); // And settings: true means it matches - assertEquals(isSpecificItem(basePath, config), true); + expect(isSpecificItem(basePath, config)).toEqual(true); // The sync logic should use branch-specific file - assertEquals(isBranchSpecificFile(branchSpecificPath), true); - assertEquals(fromBranchSpecificPath(branchSpecificPath, "main"), basePath); + expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true); + expect(fromBranchSpecificPath(branchSpecificPath, "main")).toEqual(basePath); }); -Deno.test("filtering logic: settings - when IS configured (false), skip branch-specific", () => { +test("filtering logic: settings - when IS configured (false), skip branch-specific", () => { // settings: false means type is configured but explicitly disabled const config: SpecificItemsConfig = { settings: false, @@ -738,15 +738,15 @@ Deno.test("filtering logic: settings - when IS configured (false), skip branch-s const branchSpecificPath = "settings.main.yaml"; // Settings type IS configured (even though value is false) - assertEquals(isItemTypeConfigured(basePath, config), true); + expect(isItemTypeConfigured(basePath, config)).toEqual(true); // But settings: false means it doesn't match (not a specific item) - assertEquals(isSpecificItem(basePath, config), false); + expect(isSpecificItem(basePath, config)).toEqual(false); // The sync logic should skip branch-specific file and use base }); -Deno.test("filtering logic: variables - when NOT configured, branch-specific should be ignored", () => { +test("filtering logic: variables - when NOT configured, branch-specific should be ignored", () => { // Config has folders but NOT variables const config: SpecificItemsConfig = { folders: ["f/env_*"], @@ -756,14 +756,14 @@ Deno.test("filtering logic: variables - when NOT configured, branch-specific sho const branchSpecificPath = "f/test.main.variable.yaml"; // Variable type is NOT configured - assertEquals(isItemTypeConfigured(basePath, config), false); + expect(isItemTypeConfigured(basePath, config)).toEqual(false); // Branch-specific variable files should be ignored - assertEquals(isBranchSpecificFile(branchSpecificPath), true); - assertEquals(isBranchSpecificFile(basePath), false); + expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true); + expect(isBranchSpecificFile(basePath)).toEqual(false); }); -Deno.test("filtering logic: resources - when NOT configured, branch-specific should be ignored", () => { +test("filtering logic: resources - when NOT configured, branch-specific should be ignored", () => { // Config has folders but NOT resources const config: SpecificItemsConfig = { folders: ["f/env_*"], @@ -773,13 +773,13 @@ Deno.test("filtering logic: resources - when NOT configured, branch-specific sho const branchSpecificPath = "f/db.main.resource.yaml"; // Resource type is NOT configured - assertEquals(isItemTypeConfigured(basePath, config), false); + expect(isItemTypeConfigured(basePath, config)).toEqual(false); - assertEquals(isBranchSpecificFile(branchSpecificPath), true); - assertEquals(isBranchSpecificFile(basePath), false); + expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true); + expect(isBranchSpecificFile(basePath)).toEqual(false); }); -Deno.test("filtering logic: triggers - when NOT configured, branch-specific should be ignored", () => { +test("filtering logic: triggers - when NOT configured, branch-specific should be ignored", () => { // Config has folders but NOT triggers const config: SpecificItemsConfig = { folders: ["f/env_*"], @@ -789,10 +789,10 @@ Deno.test("filtering logic: triggers - when NOT configured, branch-specific shou const branchSpecificPath = "f/webhook.main.http_trigger.yaml"; // Trigger type is NOT configured - assertEquals(isItemTypeConfigured(basePath, config), false); + expect(isItemTypeConfigured(basePath, config)).toEqual(false); - assertEquals(isBranchSpecificFile(branchSpecificPath), true); - assertEquals(isBranchSpecificFile(basePath), false); + expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true); + expect(isBranchSpecificFile(basePath)).toEqual(false); }); // ============================================================================= @@ -800,58 +800,58 @@ Deno.test("filtering logic: triggers - when NOT configured, branch-specific shou // Tests for configs that have some types configured but not others // ============================================================================= -Deno.test("mixed config: only folders configured - other types use base files", () => { +test("mixed config: only folders configured - other types use base files", () => { const config: SpecificItemsConfig = { folders: ["f/env_*"], }; // Folders IS configured - assertEquals(isItemTypeConfigured("f/env_staging/folder.meta.yaml", config), true); - assertEquals(isSpecificItem("f/env_staging/folder.meta.yaml", config), true); + expect(isItemTypeConfigured("f/env_staging/folder.meta.yaml", config)).toEqual(true); + expect(isSpecificItem("f/env_staging/folder.meta.yaml", config)).toEqual(true); // Variables, resources, triggers, settings are NOT configured - assertEquals(isItemTypeConfigured("f/test.variable.yaml", config), false); - assertEquals(isItemTypeConfigured("f/db.resource.yaml", config), false); - assertEquals(isItemTypeConfigured("f/hook.http_trigger.yaml", config), false); - assertEquals(isItemTypeConfigured("settings.yaml", config), false); + expect(isItemTypeConfigured("f/test.variable.yaml", config)).toEqual(false); + expect(isItemTypeConfigured("f/db.resource.yaml", config)).toEqual(false); + expect(isItemTypeConfigured("f/hook.http_trigger.yaml", config)).toEqual(false); + expect(isItemTypeConfigured("settings.yaml", config)).toEqual(false); }); -Deno.test("mixed config: only settings configured - other types use base files", () => { +test("mixed config: only settings configured - other types use base files", () => { const config: SpecificItemsConfig = { settings: true, }; // Settings IS configured - assertEquals(isItemTypeConfigured("settings.yaml", config), true); - assertEquals(isSpecificItem("settings.yaml", config), true); + expect(isItemTypeConfigured("settings.yaml", config)).toEqual(true); + expect(isSpecificItem("settings.yaml", config)).toEqual(true); // Other types are NOT configured - assertEquals(isItemTypeConfigured("f/test.variable.yaml", config), false); - assertEquals(isItemTypeConfigured("f/db.resource.yaml", config), false); - assertEquals(isItemTypeConfigured("f/hook.http_trigger.yaml", config), false); - assertEquals(isItemTypeConfigured("f/my_folder/folder.meta.yaml", config), false); + expect(isItemTypeConfigured("f/test.variable.yaml", config)).toEqual(false); + expect(isItemTypeConfigured("f/db.resource.yaml", config)).toEqual(false); + expect(isItemTypeConfigured("f/hook.http_trigger.yaml", config)).toEqual(false); + expect(isItemTypeConfigured("f/my_folder/folder.meta.yaml", config)).toEqual(false); }); -Deno.test("mixed config: variables and folders configured - resources and triggers use base", () => { +test("mixed config: variables and folders configured - resources and triggers use base", () => { const config: SpecificItemsConfig = { variables: ["f/**"], folders: ["f/env_*"], }; // Variables IS configured - assertEquals(isItemTypeConfigured("f/test.variable.yaml", config), true); - assertEquals(isSpecificItem("f/test.variable.yaml", config), true); + expect(isItemTypeConfigured("f/test.variable.yaml", config)).toEqual(true); + expect(isSpecificItem("f/test.variable.yaml", config)).toEqual(true); // Folders IS configured (path matches) - assertEquals(isItemTypeConfigured("f/env_staging/folder.meta.yaml", config), true); - assertEquals(isSpecificItem("f/env_staging/folder.meta.yaml", config), true); + expect(isItemTypeConfigured("f/env_staging/folder.meta.yaml", config)).toEqual(true); + expect(isSpecificItem("f/env_staging/folder.meta.yaml", config)).toEqual(true); // Folders IS configured but path doesn't match - assertEquals(isItemTypeConfigured("f/other/folder.meta.yaml", config), true); - assertEquals(isSpecificItem("f/other/folder.meta.yaml", config), false); + expect(isItemTypeConfigured("f/other/folder.meta.yaml", config)).toEqual(true); + expect(isSpecificItem("f/other/folder.meta.yaml", config)).toEqual(false); // Resources and triggers are NOT configured - assertEquals(isItemTypeConfigured("f/db.resource.yaml", config), false); - assertEquals(isItemTypeConfigured("f/hook.http_trigger.yaml", config), false); - assertEquals(isItemTypeConfigured("settings.yaml", config), false); + expect(isItemTypeConfigured("f/db.resource.yaml", config)).toEqual(false); + expect(isItemTypeConfigured("f/hook.http_trigger.yaml", config)).toEqual(false); + expect(isItemTypeConfigured("settings.yaml", config)).toEqual(false); }); diff --git a/cli/test/standalone_commands.test.ts b/cli/test/standalone_commands.test.ts new file mode 100644 index 0000000000..106e4aaeb1 --- /dev/null +++ b/cli/test/standalone_commands.test.ts @@ -0,0 +1,514 @@ +/** + * Integration tests for standalone CLI commands that previously had zero coverage. + * + * Tests: + * - `wmill folder` (list) + * - `wmill schedule` (list with data) + * - `wmill resource-type list` and `wmill resource-type push` + * - `wmill script show`, `wmill script run`, `wmill script bootstrap` + * - `wmill user` (list, add, remove) + */ + +import { expect, test, describe } from "bun:test"; +import { writeFile, mkdir, stat, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { withTestBackend, type TestBackend } from "./test_backend.ts"; +import { shouldSkipOnCI } from "./cargo_backend.ts"; +import { addWorkspace } from "../workspace.ts"; + +async function setupWorkspaceProfile(backend: TestBackend): Promise { + await addWorkspace( + { + remote: backend.baseUrl, + workspaceId: backend.workspace, + name: "localhost_test", + token: backend.token!, + }, + { force: true, configDir: backend.testConfigDir } + ); +} + +/** Create a script on the remote via API and return its path */ +async function createRemoteScript( + backend: TestBackend, + scriptPath: string, + content: string = 'export async function main() { return "hello"; }' +): Promise { + const resp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: scriptPath, + content, + language: "bun", + summary: "Test script", + description: "Created by integration test", + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + required: [], + }, + }), + } + ); + expect(resp.status).toBeLessThan(300); + await resp.text(); +} + +// ============================================================================= +// Folder List +// ============================================================================= + +describe("folder list command", () => { + test("lists seeded folders", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand(["folder"], tempDir); + + expect(result.code).toEqual(0); + // seedTestData creates a "test" folder + expect(result.stdout).toContain("test"); + // Table headers should be present + expect(result.stdout).toContain("Name"); + }); + }); +}); + +// ============================================================================= +// Schedule List +// ============================================================================= + +describe("schedule list command", () => { + test("lists a schedule created via API", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const scriptPath = `f/test/sched_list_target_${uniqueId}`; + const schedulePath = `f/test/sched_list_${uniqueId}`; + + // Create target script + await createRemoteScript(backend, scriptPath); + + // Create schedule via API + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/schedules/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: schedulePath, + schedule: "0 0 12 * * *", + script_path: scriptPath, + is_flow: false, + args: {}, + enabled: false, + timezone: "UTC", + }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + // List schedules via CLI + const result = await backend.runCLICommand(["schedule"], tempDir); + + expect(result.code).toEqual(0); + expect(result.stdout).toContain(schedulePath); + expect(result.stdout).toContain("0 0 12 * * *"); + }); + }); +}); + +// ============================================================================= +// Resource Type List & Push +// ============================================================================= + +describe("resource-type commands", () => { + test("list returns exit code 0", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["resource-type", "list"], + tempDir + ); + + expect(result.code).toEqual(0); + // Table headers should be present + expect(result.stdout).toContain("Name"); + }); + }); + + test("push creates a new resource type", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const rtName = `test_rt_${uniqueId}`; + + // Create a resource type JSON file + const rtFile = join(tempDir, `${rtName}.resource-type.json`); + await writeFile( + rtFile, + JSON.stringify({ + schema: { + type: "object", + properties: { + host: { type: "string" }, + port: { type: "integer" }, + }, + }, + description: "Test resource type from integration test", + }), + "utf-8" + ); + + // Push via CLI — the name argument must include the .resource-type.json suffix + const pushResult = await backend.runCLICommand( + ["resource-type", "push", rtFile, `${rtName}.resource-type.json`], + tempDir + ); + + expect(pushResult.code).toEqual(0); + + // Verify via API + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/resources/type/get/${rtName}` + ); + expect(apiResp.status).toEqual(200); + const rtData = await apiResp.json(); + expect(rtData.name).toBe(rtName); + expect(rtData.schema).toBeDefined(); + expect(rtData.schema.properties.host.type).toBe("string"); + }); + }); + + test("push updates an existing resource type", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const rtName = `test_rt_upd_${uniqueId}`; + + // Create resource type via API first + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/resources/type/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: rtName, + schema: { + type: "object", + properties: { old_field: { type: "string" } }, + }, + description: "Original", + }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + // Create updated resource type file + const rtFile = join(tempDir, `${rtName}.resource-type.json`); + await writeFile( + rtFile, + JSON.stringify({ + schema: { + type: "object", + properties: { + new_field: { type: "number" }, + }, + }, + description: "Updated description", + }), + "utf-8" + ); + + // Push update via CLI — the name argument must include the .resource-type.json suffix + const pushResult = await backend.runCLICommand( + ["resource-type", "push", rtFile, `${rtName}.resource-type.json`], + tempDir + ); + + expect(pushResult.code).toEqual(0); + + // Verify the update via API + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/resources/type/get/${rtName}` + ); + expect(apiResp.status).toEqual(200); + const rtData = await apiResp.json(); + expect(rtData.description).toBe("Updated description"); + expect(rtData.schema.properties.new_field.type).toBe("number"); + }); + }); +}); + +// ============================================================================= +// Script Show +// ============================================================================= + +describe("script show command", () => { + test("shows script content", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const scriptPath = `f/test/show_script_${uniqueId}`; + const scriptContent = `export async function main() { return "show_test_${uniqueId}"; }`; + + await createRemoteScript(backend, scriptPath, scriptContent); + + const result = await backend.runCLICommand( + ["script", "show", scriptPath], + tempDir + ); + + expect(result.code).toEqual(0); + // Should display the script content + const output = result.stdout + result.stderr; + expect(output).toContain(`show_test_${uniqueId}`); + expect(output).toContain(scriptPath); + }); + }); +}); + +// ============================================================================= +// Script Run +// ============================================================================= + +describe("script run command", () => { + test("runs a script and returns result", { timeout: 60000 }, async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const scriptPath = `f/test/run_script_${uniqueId}`; + const scriptContent = `export async function main() { return { value: "run_result_${uniqueId}" }; }`; + + await createRemoteScript(backend, scriptPath, scriptContent); + + const result = await backend.runCLICommand( + ["script", "run", scriptPath, "--silent"], + tempDir + ); + + expect(result.code).toEqual(0); + expect(result.stdout).toContain(`run_result_${uniqueId}`); + }); + }); +}); + +// ============================================================================= +// Script Bootstrap +// ============================================================================= + +describe("script bootstrap command", () => { + test("creates TypeScript script files", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + // Create a wmill.yaml so bootstrap can read config + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + [ + "script", + "bootstrap", + "f/test/new_script", + "bun", + "--summary", + "My new script", + ], + tempDir + ); + + expect(result.code).toEqual(0); + + // Verify the code file was created + const codeStat = await stat(join(tempDir, "f/test/new_script.ts")); + expect(codeStat.isFile()).toBe(true); + + // Verify the metadata file was created + const metaStat = await stat( + join(tempDir, "f/test/new_script.script.yaml") + ); + expect(metaStat.isFile()).toBe(true); + + // Verify metadata content + const metaContent = await readFile( + join(tempDir, "f/test/new_script.script.yaml"), + "utf-8" + ); + expect(metaContent).toContain("My new script"); + }); + }); + + test("creates Python script files", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["script", "bootstrap", "f/test/py_script", "python3"], + tempDir + ); + + expect(result.code).toEqual(0); + + const codeStat = await stat(join(tempDir, "f/test/py_script.py")); + expect(codeStat.isFile()).toBe(true); + + const metaStat = await stat( + join(tempDir, "f/test/py_script.script.yaml") + ); + expect(metaStat.isFile()).toBe(true); + }); + }); + + test("creates Bash script files", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["script", "bootstrap", "f/test/bash_script", "bash"], + tempDir + ); + + expect(result.code).toEqual(0); + + const codeStat = await stat(join(tempDir, "f/test/bash_script.sh")); + expect(codeStat.isFile()).toBe(true); + }); + }); + + test("creates Go script files", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["script", "bootstrap", "f/test/go_script", "go"], + tempDir + ); + + expect(result.code).toEqual(0); + + const codeStat = await stat(join(tempDir, "f/test/go_script.go")); + expect(codeStat.isFile()).toBe(true); + }); + }); +}); + +// ============================================================================= +// User List, Add, Remove +// ============================================================================= + +describe("user commands", () => { + test("list shows existing admin user", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand(["user"], tempDir); + + expect(result.code).toEqual(0); + // The admin user is always created by the test backend + expect(result.stdout).toContain("admin@windmill.dev"); + // Table headers + expect(result.stdout).toContain("email"); + }); + }); + + test.skipIf(shouldSkipOnCI())("add creates a new user and remove deletes it", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const email = `testuser_${uniqueId}@example.com`; + const password = "testpass123"; + + // Add user + const addResult = await backend.runCLICommand( + ["user", "add", email, password], + tempDir + ); + expect(addResult.code).toEqual(0); + + // Verify the user appears in the list + const listResult = await backend.runCLICommand(["user"], tempDir); + expect(listResult.code).toEqual(0); + expect(listResult.stdout).toContain(email); + + // Remove user + const removeResult = await backend.runCLICommand( + ["user", "remove", email], + tempDir + ); + expect(removeResult.code).toEqual(0); + + // Verify the user no longer appears + const listAfterResult = await backend.runCLICommand(["user"], tempDir); + expect(listAfterResult.code).toEqual(0); + expect(listAfterResult.stdout).not.toContain(email); + }); + }); + + test.skipIf(shouldSkipOnCI())("add with --superadmin flag creates superadmin user", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const email = `superuser_${uniqueId}@example.com`; + const password = "superpass123"; + + // Add superadmin user + const addResult = await backend.runCLICommand( + ["user", "add", email, password, "--superadmin"], + tempDir + ); + expect(addResult.code).toEqual(0); + + // Verify user exists and is superadmin + const listResult = await backend.runCLICommand(["user"], tempDir); + expect(listResult.code).toEqual(0); + expect(listResult.stdout).toContain(email); + + // Clean up + await backend.runCLICommand(["user", "remove", email], tempDir); + }); + }); +}); diff --git a/cli/test/sync_config_resolution.test.ts b/cli/test/sync_config_resolution.test.ts index a24d3e7c15..d5583b010b 100644 --- a/cli/test/sync_config_resolution.test.ts +++ b/cli/test/sync_config_resolution.test.ts @@ -1,4 +1,5 @@ -import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; +import { writeFile } from "node:fs/promises"; import { readConfigFile, getEffectiveSettings } from "../src/core/conf.ts"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; @@ -26,17 +27,13 @@ async function setupWorkspaceProfile(backend: any): Promise { // INTEGRATION TESTS WITH REAL BACKEND // ============================================================================= -Deno.test({ - name: "Integration: wmill.yaml configuration produces expected results", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Integration: wmill.yaml configuration produces expected results", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace profile with name "localhost_test" await setupWorkspaceProfile(backend); // Create wmill.yaml with settings - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - f/** - settings.yaml @@ -46,7 +43,7 @@ skipVariables: true skipResources: true includeSettings: true includeSchedules: true -includeTriggers: true`); +includeTriggers: true`, "utf-8"); // Test pull with wmill.yaml configuration const yamlResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir); @@ -56,7 +53,7 @@ includeTriggers: true`); console.log("Stdout:", yamlResult.stdout); console.log("Stderr:", yamlResult.stderr); } - assertEquals(yamlResult.code, 0); + expect(yamlResult.code).toEqual(0); // Extract JSON from CLI output (skip log messages) const yamlData = parseJsonFromCLIOutput(yamlResult.stdout); @@ -65,7 +62,7 @@ includeTriggers: true`); const hasSettings = (yamlData.changes || []).some((change: any) => change.type === 'added' && change.path === 'settings.yaml' ); - assertEquals(hasSettings, true); + expect(hasSettings).toEqual(true); // Should NOT include resources or variables (due to skip flags) const hasResources = (yamlData.changes || []).some((change: any) => @@ -74,72 +71,64 @@ includeTriggers: true`); const hasVariables = (yamlData.changes || []).some((change: any) => change.type === 'added' && change.path?.includes('.variable.yaml') ); - assertEquals(hasResources, false); - assertEquals(hasVariables, false); + expect(hasResources).toEqual(false); + expect(hasVariables).toEqual(false); }); -}}); +}); -Deno.test({ - name: "Integration: settings.yaml inclusion respects includeSettings flag", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Integration: settings.yaml inclusion respects includeSettings flag", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace profile with name "localhost_test" await setupWorkspaceProfile(backend); // Test 1: includeSettings: true should include settings.yaml - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" -includeSettings: true`); +includeSettings: true`, "utf-8"); const includeResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir); - assertEquals(includeResult.code, 0); + expect(includeResult.code).toEqual(0); // Extract JSON from CLI output (skip log messages) const includeData = parseJsonFromCLIOutput(includeResult.stdout); const hasSettingsInclude = (includeData.changes || []).some((change: any) => change.type === 'added' && change.path === 'settings.yaml' ); - assertEquals(hasSettingsInclude, true); + expect(hasSettingsInclude).toEqual(true); // Test 2: includeSettings: false should NOT include settings.yaml - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" -includeSettings: false`); +includeSettings: false`, "utf-8"); const excludeResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir); - assertEquals(excludeResult.code, 0); + expect(excludeResult.code).toEqual(0); // Extract JSON from CLI output (skip log messages) const excludeData = parseJsonFromCLIOutput(excludeResult.stdout); const hasSettingsExclude = (excludeData.changes || []).some((change: any) => change.type === 'added' && change.path === 'settings.yaml' ); - assertEquals(hasSettingsExclude, false); + expect(hasSettingsExclude).toEqual(false); }); -}}); +}); -Deno.test({ - name: "Integration: resource/variable filtering respects skip flags", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Integration: resource/variable filtering respects skip flags", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace profile with name "localhost_test" await setupWorkspaceProfile(backend); // Test skipResources: true - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" skipResources: true -skipVariables: false`); +skipVariables: false`, "utf-8"); const result = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir); - assertEquals(result.code, 0); + expect(result.code).toEqual(0); // Extract JSON from CLI output (skip log messages) const data = parseJsonFromCLIOutput(result.stdout); @@ -148,42 +137,38 @@ skipVariables: false`); const hasResources = (data.changes || []).some((change: any) => change.type === 'added' && change.path?.includes('.resource.yaml') ); - assertEquals(hasResources, false); + expect(hasResources).toEqual(false); // Should include variables (not skipped) const hasVariables = (data.changes || []).some((change: any) => change.type === 'added' && change.path?.includes('.variable.yaml') ); - assertEquals(hasVariables, true); + expect(hasVariables).toEqual(true); }); -}}); +}); // ============================================================================= // CLI FLAG OVERRIDE TESTS // Tests for CLI flags overriding configuration file settings // ============================================================================= -Deno.test({ - name: "CLI skip flags override wmill.yaml configuration", - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("CLI skip flags override wmill.yaml configuration", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace profile with name "localhost_test" await setupWorkspaceProfile(backend); // Create wmill.yaml that INCLUDES resources by default (skipResources: false) - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - f/** - u/** skipResources: false skipResourceTypes: false -includeSettings: true`); +includeSettings: true`, "utf-8"); // Test 1: Without CLI flags - should respect wmill.yaml (include resources) const configResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir); - assertEquals(configResult.code, 0); + expect(configResult.code).toEqual(0); const configData = parseJsonFromCLIOutput(configResult.stdout); @@ -192,7 +177,7 @@ includeSettings: true`); const hasResources = (configData.changes || []).some((change: any) => change.type === 'added' && change.path?.includes('.resource.yaml') ); - assertEquals(hasResources, true, "Resources should be included by wmill.yaml config"); + expect(hasResources).toEqual(true); // Test 2: With CLI --skip-resources flag - should override wmill.yaml to skip resources const overrideResult = await backend.runCLICommand([ @@ -200,7 +185,7 @@ includeSettings: true`); '--skip-resources', // CLI flag should override config to skip resources '--skip-resource-types' // CLI flag should override config to skip resource types ], tempDir); - assertEquals(overrideResult.code, 0); + expect(overrideResult.code).toEqual(0); const overrideData = parseJsonFromCLIOutput(overrideResult.stdout); @@ -208,12 +193,12 @@ includeSettings: true`); const hasResourcesOverride = (overrideData.changes || []).some((change: any) => change.type === 'added' && change.path?.includes('.resource.yaml') ); - assertEquals(hasResourcesOverride, false, "CLI --skip-resources flag should override wmill.yaml to exclude resources"); + expect(hasResourcesOverride).toEqual(false); // Should NOT include resource types (CLI flag overrides config) const hasResourceTypesOverride = (overrideData.changes || []).some((change: any) => change.type === 'added' && change.path?.includes('.resource-type.yaml') ); - assertEquals(hasResourceTypesOverride, false, "CLI --skip-resource-types flag should override wmill.yaml to exclude resource types"); + expect(hasResourceTypesOverride).toEqual(false); }); -}}); +}); diff --git a/cli/test/sync_pull_push.test.ts b/cli/test/sync_pull_push.test.ts index fdbb65cccc..7abe61edab 100644 --- a/cli/test/sync_pull_push.test.ts +++ b/cli/test/sync_pull_push.test.ts @@ -5,11 +5,13 @@ * containing every kind of Windmill resource type. */ -import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; -import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts"; -import * as path from "https://deno.land/std@0.224.0/path/mod.ts"; -import { SEPARATOR as SEP } from "https://deno.land/std@0.224.0/path/mod.ts"; -import { JSZip } from "../deps.ts"; +import { expect, test, describe } from "bun:test"; +import * as path from "@std/path"; +import { SEPARATOR as SEP } from "@std/path"; +import { writeFile, readFile, readdir, rm, mkdir, mkdtemp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import JSZip from "jszip"; import { getFolderSuffix, getMetadataFileName, @@ -334,7 +336,7 @@ async function createLocalFilesystem(baseDir: string): Promise { // Create folder structure const folders = ["f/scripts", "f/flows", "f/apps", "f/resources"]; for (const folder of folders) { - await ensureDir(path.join(baseDir, folder)); + await mkdir(path.join(baseDir, folder), { recursive: true }); } // Create scripts @@ -347,35 +349,37 @@ async function createLocalFilesystem(baseDir: string): Promise { ]; for (const script of scripts) { - await Deno.writeTextFile( + await writeFile( path.join(baseDir, script.contentFile.path), script.contentFile.content, + "utf-8", ); - await Deno.writeTextFile( + await writeFile( path.join(baseDir, script.metadataFile.path), script.metadataFile.content, + "utf-8", ); } // Create flows const flowFixture = createFlowFixture("f/flows/test_flow"); - await ensureDir(path.join(baseDir, `f/flows/test_flow${getFolderSuffix("flow")}`)); + await mkdir(path.join(baseDir, `f/flows/test_flow${getFolderSuffix("flow")}`), { recursive: true }); for (const file of Object.values(flowFixture)) { - await Deno.writeTextFile(path.join(baseDir, file.path), file.content); + await writeFile(path.join(baseDir, file.path), file.content, "utf-8"); } // Create apps const appFixture = createAppFixture("f/apps/test_app"); - await ensureDir(path.join(baseDir, `f/apps/test_app${getFolderSuffix("app")}`)); + await mkdir(path.join(baseDir, `f/apps/test_app${getFolderSuffix("app")}`), { recursive: true }); for (const file of Object.values(appFixture)) { - await Deno.writeTextFile(path.join(baseDir, file.path), file.content); + await writeFile(path.join(baseDir, file.path), file.content, "utf-8"); } // Create raw apps const rawAppFixture = createRawAppFixture("f/apps/test_raw_app"); - await ensureDir(path.join(baseDir, `f/apps/test_raw_app${getFolderSuffix("raw_app")}`)); + await mkdir(path.join(baseDir, `f/apps/test_raw_app${getFolderSuffix("raw_app")}`), { recursive: true }); for (const file of Object.values(rawAppFixture)) { - await Deno.writeTextFile(path.join(baseDir, file.path), file.content); + await writeFile(path.join(baseDir, file.path), file.content, "utf-8"); } // Create resources @@ -393,7 +397,7 @@ async function createLocalFilesystem(baseDir: string): Promise { ]; for (const resource of resources) { - await Deno.writeTextFile(path.join(baseDir, resource.path), resource.content); + await writeFile(path.join(baseDir, resource.path), resource.content, "utf-8"); } // Create variables @@ -403,13 +407,13 @@ async function createLocalFilesystem(baseDir: string): Promise { ]; for (const variable of variables) { - await Deno.writeTextFile(path.join(baseDir, variable.path), variable.content); + await writeFile(path.join(baseDir, variable.path), variable.content, "utf-8"); } // Create folder metadata - await ensureDir(path.join(baseDir, "f")); + await mkdir(path.join(baseDir, "f"), { recursive: true }); const folderMeta = createFolderFixture("f"); - await Deno.writeTextFile(path.join(baseDir, folderMeta.path), folderMeta.content); + await writeFile(path.join(baseDir, folderMeta.path), folderMeta.content, "utf-8"); } /** @@ -435,16 +439,17 @@ async function readDirRecursive( ): Promise> { const files: Record = {}; - for await (const entry of Deno.readDir(dir)) { + const entries = await readdir(dir, { withFileTypes: true }); + for (const entry of entries) { const fullPath = path.join(dir, entry.name); // Normalize path separators to forward slashes for cross-platform compatibility const relativePath = fullPath.substring(baseDir.length + 1).replaceAll("\\", "/"); - if (entry.isDirectory) { + if (entry.isDirectory()) { const subFiles = await readDirRecursive(fullPath, baseDir); Object.assign(files, subFiles); } else { - files[relativePath] = await Deno.readTextFile(fullPath); + files[relativePath] = await readFile(fullPath, "utf-8"); } } @@ -455,7 +460,7 @@ async function readDirRecursive( * Creates a temporary directory for testing */ async function createTempDir(): Promise { - return await Deno.makeTempDir({ prefix: "wmill_sync_test_" }); + return await mkdtemp(join(tmpdir(), "wmill_sync_test_")); } /** @@ -463,7 +468,7 @@ async function createTempDir(): Promise { */ async function cleanupTempDir(dir: string): Promise { try { - await Deno.remove(dir, { recursive: true }); + await rm(dir, { recursive: true }); } catch { // Ignore cleanup errors } @@ -473,47 +478,47 @@ async function cleanupTempDir(dir: string): Promise { // Tests // ============================================================================= -Deno.test("Resource folder suffixes are correct", () => { - assertEquals(getFolderSuffix("flow"), ".flow"); - assertEquals(getFolderSuffix("app"), ".app"); - assertEquals(getFolderSuffix("raw_app"), ".raw_app"); +test("Resource folder suffixes are correct", () => { + expect(getFolderSuffix("flow")).toEqual(".flow"); + expect(getFolderSuffix("app")).toEqual(".app"); + expect(getFolderSuffix("raw_app")).toEqual(".raw_app"); }); -Deno.test("Metadata file names are correct", () => { - assertEquals(getMetadataFileName("flow", "yaml"), "flow.yaml"); - assertEquals(getMetadataFileName("flow", "json"), "flow.json"); - assertEquals(getMetadataFileName("app", "yaml"), "app.yaml"); - assertEquals(getMetadataFileName("raw_app", "yaml"), "raw_app.yaml"); +test("Metadata file names are correct", () => { + expect(getMetadataFileName("flow", "yaml")).toEqual("flow.yaml"); + expect(getMetadataFileName("flow", "json")).toEqual("flow.json"); + expect(getMetadataFileName("app", "yaml")).toEqual("app.yaml"); + expect(getMetadataFileName("raw_app", "yaml")).toEqual("raw_app.yaml"); }); -Deno.test("buildFolderPath creates correct paths", () => { - assertEquals(buildFolderPath("my_flow", "flow"), "my_flow.flow"); - assertEquals(buildFolderPath("f/test/my_app", "app"), "f/test/my_app.app"); - assertEquals(buildFolderPath("u/admin/raw_app", "raw_app"), "u/admin/raw_app.raw_app"); +test("buildFolderPath creates correct paths", () => { + expect(buildFolderPath("my_flow", "flow")).toEqual("my_flow.flow"); + expect(buildFolderPath("f/test/my_app", "app")).toEqual("f/test/my_app.app"); + expect(buildFolderPath("u/admin/raw_app", "raw_app")).toEqual("u/admin/raw_app.raw_app"); }); // ============================================================================= // nonDottedPaths Tests - API format detection and transformation // ============================================================================= -Deno.test("Metadata file detection works with dotted format (default)", () => { +test("Metadata file detection works with dotted format (default)", () => { // Ensure we're in default mode setNonDottedPaths(false); // API always returns dotted format - assert(isFlowMetadataFile("f/my_flow.flow.json"), "Should detect .flow.json"); - assert(isFlowMetadataFile("f/my_flow.flow.yaml"), "Should detect .flow.yaml"); - assert(isAppMetadataFile("f/my_app.app.json"), "Should detect .app.json"); - assert(isAppMetadataFile("f/my_app.app.yaml"), "Should detect .app.yaml"); - assert(isRawAppMetadataFile("f/my_raw.raw_app.json"), "Should detect .raw_app.json"); - assert(isRawAppMetadataFile("f/my_raw.raw_app.yaml"), "Should detect .raw_app.yaml"); + expect(isFlowMetadataFile("f/my_flow.flow.json")).toBeTruthy(); + expect(isFlowMetadataFile("f/my_flow.flow.yaml")).toBeTruthy(); + expect(isAppMetadataFile("f/my_app.app.json")).toBeTruthy(); + expect(isAppMetadataFile("f/my_app.app.yaml")).toBeTruthy(); + expect(isRawAppMetadataFile("f/my_raw.raw_app.json")).toBeTruthy(); + expect(isRawAppMetadataFile("f/my_raw.raw_app.yaml")).toBeTruthy(); // Non-matching should return false - assert(!isFlowMetadataFile("f/my_script.ts"), "Should not detect script file"); - assert(!isAppMetadataFile("f/my_script.ts"), "Should not detect script file"); + expect(!isFlowMetadataFile("f/my_script.ts")).toBeTruthy(); + expect(!isAppMetadataFile("f/my_script.ts")).toBeTruthy(); }); -Deno.test("Metadata file detection works with nonDottedPaths=true", () => { +test("Metadata file detection works with nonDottedPaths=true", () => { // Store original value const wasNonDotted = getNonDottedPaths(); @@ -521,309 +526,281 @@ Deno.test("Metadata file detection works with nonDottedPaths=true", () => { setNonDottedPaths(true); // API format (dotted) should still be detected - assert(isFlowMetadataFile("f/my_flow.flow.json"), "Should detect API format .flow.json"); - assert(isAppMetadataFile("f/my_app.app.json"), "Should detect API format .app.json"); - assert(isRawAppMetadataFile("f/my_raw.raw_app.json"), "Should detect API format .raw_app.json"); + expect(isFlowMetadataFile("f/my_flow.flow.json")).toBeTruthy(); + expect(isAppMetadataFile("f/my_app.app.json")).toBeTruthy(); + expect(isRawAppMetadataFile("f/my_raw.raw_app.json")).toBeTruthy(); // Local format (non-dotted) should also be detected - assert(isFlowMetadataFile("f/my_flow__flow.json"), "Should detect local format __flow.json"); - assert(isFlowMetadataFile("f/my_flow__flow.yaml"), "Should detect local format __flow.yaml"); - assert(isAppMetadataFile("f/my_app__app.json"), "Should detect local format __app.json"); - assert(isRawAppMetadataFile("f/my_raw__raw_app.json"), "Should detect local format __raw_app.json"); + expect(isFlowMetadataFile("f/my_flow__flow.json")).toBeTruthy(); + expect(isFlowMetadataFile("f/my_flow__flow.yaml")).toBeTruthy(); + expect(isAppMetadataFile("f/my_app__app.json")).toBeTruthy(); + expect(isRawAppMetadataFile("f/my_raw__raw_app.json")).toBeTruthy(); } finally { // Restore original value setNonDottedPaths(wasNonDotted); } }); -Deno.test("transformJsonPathToDir transforms API format to local format", () => { +test("transformJsonPathToDir transforms API format to local format", () => { // Store original value const wasNonDotted = getNonDottedPaths(); try { // Test with dotted paths (default) setNonDottedPaths(false); - assertEquals( - transformJsonPathToDir("f/my_flow.flow.json", "flow"), - "f/my_flow.flow", - "Should transform dotted API format to dotted local format" - ); - assertEquals( - transformJsonPathToDir("f/my_app.app.json", "app"), - "f/my_app.app", - "Should transform app correctly" - ); - assertEquals( - transformJsonPathToDir("f/my_raw.raw_app.json", "raw_app"), - "f/my_raw.raw_app", - "Should transform raw_app correctly" - ); + expect(transformJsonPathToDir("f/my_flow.flow.json", "flow")).toEqual("f/my_flow.flow"); + expect(transformJsonPathToDir("f/my_app.app.json", "app")).toEqual("f/my_app.app"); + expect(transformJsonPathToDir("f/my_raw.raw_app.json", "raw_app")).toEqual("f/my_raw.raw_app"); // Test with non-dotted paths setNonDottedPaths(true); - assertEquals( - transformJsonPathToDir("f/my_flow.flow.json", "flow"), - "f/my_flow__flow", - "Should transform dotted API format to non-dotted local format" - ); - assertEquals( - transformJsonPathToDir("f/my_app.app.json", "app"), - "f/my_app__app", - "Should transform app to non-dotted format" - ); - assertEquals( - transformJsonPathToDir("f/my_raw.raw_app.json", "raw_app"), - "f/my_raw__raw_app", - "Should transform raw_app to non-dotted format" - ); + expect(transformJsonPathToDir("f/my_flow.flow.json", "flow")).toEqual("f/my_flow__flow"); + expect(transformJsonPathToDir("f/my_app.app.json", "app")).toEqual("f/my_app__app"); + expect(transformJsonPathToDir("f/my_raw.raw_app.json", "raw_app")).toEqual("f/my_raw__raw_app"); // Non-matching paths should be returned unchanged - assertEquals( - transformJsonPathToDir("f/my_script.ts", "flow"), - "f/my_script.ts", - "Should return non-matching path unchanged" - ); + expect(transformJsonPathToDir("f/my_script.ts", "flow")).toEqual("f/my_script.ts"); } finally { // Restore original value setNonDottedPaths(wasNonDotted); } }); -Deno.test("getFolderSuffix returns correct suffix based on nonDottedPaths setting", () => { +test("getFolderSuffix returns correct suffix based on nonDottedPaths setting", () => { // Store original value const wasNonDotted = getNonDottedPaths(); try { setNonDottedPaths(false); - assertEquals(getFolderSuffix("flow"), ".flow"); - assertEquals(getFolderSuffix("app"), ".app"); - assertEquals(getFolderSuffix("raw_app"), ".raw_app"); + expect(getFolderSuffix("flow")).toEqual(".flow"); + expect(getFolderSuffix("app")).toEqual(".app"); + expect(getFolderSuffix("raw_app")).toEqual(".raw_app"); setNonDottedPaths(true); - assertEquals(getFolderSuffix("flow"), "__flow"); - assertEquals(getFolderSuffix("app"), "__app"); - assertEquals(getFolderSuffix("raw_app"), "__raw_app"); + expect(getFolderSuffix("flow")).toEqual("__flow"); + expect(getFolderSuffix("app")).toEqual("__app"); + expect(getFolderSuffix("raw_app")).toEqual("__raw_app"); } finally { // Restore original value setNonDottedPaths(wasNonDotted); } }); -Deno.test("newPathAssigner with skipInlineScriptSuffix removes .inline_script. from paths", () => { +test("newPathAssigner with skipInlineScriptSuffix removes .inline_script. from paths", () => { // Test default behavior (with .inline_script. suffix) const defaultAssigner = newPathAssigner("bun"); const [defaultPath, defaultExt] = defaultAssigner.assignPath("my_script", "bun"); - assertEquals(defaultPath, "my_script.inline_script."); - assertEquals(defaultExt, "ts"); + expect(defaultPath).toEqual("my_script.inline_script."); + expect(defaultExt).toEqual("ts"); // Test with skipInlineScriptSuffix = false (explicit) const withSuffixAssigner = newPathAssigner("bun", { skipInlineScriptSuffix: false }); const [withSuffixPath, withSuffixExt] = withSuffixAssigner.assignPath("another_script", "python3"); - assertEquals(withSuffixPath, "another_script.inline_script."); - assertEquals(withSuffixExt, "py"); + expect(withSuffixPath).toEqual("another_script.inline_script."); + expect(withSuffixExt).toEqual("py"); // Test with skipInlineScriptSuffix = true (no .inline_script. suffix) const noSuffixAssigner = newPathAssigner("bun", { skipInlineScriptSuffix: true }); const [noSuffixPath, noSuffixExt] = noSuffixAssigner.assignPath("clean_script", "bun"); - assertEquals(noSuffixPath, "clean_script."); - assertEquals(noSuffixExt, "ts"); + expect(noSuffixPath).toEqual("clean_script."); + expect(noSuffixExt).toEqual("ts"); // Test with skipInlineScriptSuffix = true and different language const noSuffixPyAssigner = newPathAssigner("bun", { skipInlineScriptSuffix: true }); const [noSuffixPyPath, noSuffixPyExt] = noSuffixPyAssigner.assignPath("python_script", "python3"); - assertEquals(noSuffixPyPath, "python_script."); - assertEquals(noSuffixPyExt, "py"); + expect(noSuffixPyPath).toEqual("python_script."); + expect(noSuffixPyExt).toEqual("py"); }); -Deno.test("newPathAssigner generates unique paths for duplicate names", () => { +test("newPathAssigner generates unique paths for duplicate names", () => { const assigner = newPathAssigner("bun", { skipInlineScriptSuffix: true }); // First script const [path1, ext1] = assigner.assignPath("my_script", "bun"); - assertEquals(path1, "my_script."); - assertEquals(ext1, "ts"); + expect(path1).toEqual("my_script."); + expect(ext1).toEqual("ts"); // Second script with same name should get counter const [path2, ext2] = assigner.assignPath("my_script", "bun"); - assertEquals(path2, "my_script_1."); - assertEquals(ext2, "ts"); + expect(path2).toEqual("my_script_1."); + expect(ext2).toEqual("ts"); // Third script with same name should get incremented counter const [path3, ext3] = assigner.assignPath("my_script", "python3"); - assertEquals(path3, "my_script_2."); - assertEquals(ext3, "py"); + expect(path3).toEqual("my_script_2."); + expect(ext3).toEqual("py"); }); -Deno.test("isAppInlineScriptPath detects app inline scripts correctly", () => { +test("isAppInlineScriptPath detects app inline scripts correctly", () => { // Store original value const wasNonDotted = getNonDottedPaths(); try { // Test with dotted paths (default) setNonDottedPaths(false); - assert(isAppInlineScriptPath("f/my_app.app/my_script.ts"), "Should detect script in .app folder"); - assert(isAppInlineScriptPath("f/my_app.app/app.yaml"), "Should detect metadata in .app folder"); - assert(!isAppInlineScriptPath("f/my_script.ts"), "Should not detect standalone script"); - assert(!isAppInlineScriptPath("f/my_flow.flow/flow.yaml"), "Should not detect flow files"); + expect(isAppInlineScriptPath("f/my_app.app/my_script.ts")).toBeTruthy(); + expect(isAppInlineScriptPath("f/my_app.app/app.yaml")).toBeTruthy(); + expect(!isAppInlineScriptPath("f/my_script.ts")).toBeTruthy(); + expect(!isAppInlineScriptPath("f/my_flow.flow/flow.yaml")).toBeTruthy(); // Test with non-dotted paths setNonDottedPaths(true); - assert(isAppInlineScriptPath("f/my_app__app/my_script.ts"), "Should detect script in __app folder"); - assert(isAppInlineScriptPath("f/my_app__app/app.yaml"), "Should detect metadata in __app folder"); - assert(!isAppInlineScriptPath("f/my_script.ts"), "Should not detect standalone script"); - assert(!isAppInlineScriptPath("f/my_flow__flow/flow.yaml"), "Should not detect flow files"); + expect(isAppInlineScriptPath("f/my_app__app/my_script.ts")).toBeTruthy(); + expect(isAppInlineScriptPath("f/my_app__app/app.yaml")).toBeTruthy(); + expect(!isAppInlineScriptPath("f/my_script.ts")).toBeTruthy(); + expect(!isAppInlineScriptPath("f/my_flow__flow/flow.yaml")).toBeTruthy(); } finally { // Restore original value setNonDottedPaths(wasNonDotted); } }); -Deno.test("isFlowInlineScriptPath detects flow inline scripts correctly", () => { +test("isFlowInlineScriptPath detects flow inline scripts correctly", () => { // Store original value const wasNonDotted = getNonDottedPaths(); try { // Test with dotted paths (default) setNonDottedPaths(false); - assert(isFlowInlineScriptPath("f/my_flow.flow/my_script.ts"), "Should detect script in .flow folder"); - assert(isFlowInlineScriptPath("f/my_flow.flow/flow.yaml"), "Should detect metadata in .flow folder"); - assert(!isFlowInlineScriptPath("f/my_script.ts"), "Should not detect standalone script"); - assert(!isFlowInlineScriptPath("f/my_app.app/app.yaml"), "Should not detect app files"); + expect(isFlowInlineScriptPath("f/my_flow.flow/my_script.ts")).toBeTruthy(); + expect(isFlowInlineScriptPath("f/my_flow.flow/flow.yaml")).toBeTruthy(); + expect(!isFlowInlineScriptPath("f/my_script.ts")).toBeTruthy(); + expect(!isFlowInlineScriptPath("f/my_app.app/app.yaml")).toBeTruthy(); // Test with non-dotted paths setNonDottedPaths(true); - assert(isFlowInlineScriptPath("f/my_flow__flow/my_script.ts"), "Should detect script in __flow folder"); - assert(isFlowInlineScriptPath("f/my_flow__flow/flow.yaml"), "Should detect metadata in __flow folder"); - assert(!isFlowInlineScriptPath("f/my_script.ts"), "Should not detect standalone script"); - assert(!isFlowInlineScriptPath("f/my_app__app/app.yaml"), "Should not detect app files"); + expect(isFlowInlineScriptPath("f/my_flow__flow/my_script.ts")).toBeTruthy(); + expect(isFlowInlineScriptPath("f/my_flow__flow/flow.yaml")).toBeTruthy(); + expect(!isFlowInlineScriptPath("f/my_script.ts")).toBeTruthy(); + expect(!isFlowInlineScriptPath("f/my_app__app/app.yaml")).toBeTruthy(); } finally { // Restore original value setNonDottedPaths(wasNonDotted); } }); -Deno.test("isRawAppBackendPath detects raw app backend paths correctly", () => { +test("isRawAppBackendPath detects raw app backend paths correctly", () => { // Store original value const wasNonDotted = getNonDottedPaths(); try { // Test with dotted paths (default) setNonDottedPaths(false); - assert(isRawAppBackendPath("f/my_app.raw_app/backend/script.ts"), "Should detect script in .raw_app/backend"); - assert(!isRawAppBackendPath("f/my_app.raw_app/index.html"), "Should not detect root files in raw_app"); - assert(!isRawAppBackendPath("f/my_script.ts"), "Should not detect standalone script"); + expect(isRawAppBackendPath("f/my_app.raw_app/backend/script.ts")).toBeTruthy(); + expect(!isRawAppBackendPath("f/my_app.raw_app/index.html")).toBeTruthy(); + expect(!isRawAppBackendPath("f/my_script.ts")).toBeTruthy(); // Test with non-dotted paths setNonDottedPaths(true); - assert(isRawAppBackendPath("f/my_app__raw_app/backend/script.ts"), "Should detect script in __raw_app/backend"); - assert(!isRawAppBackendPath("f/my_app__raw_app/index.html"), "Should not detect root files in raw_app"); - assert(!isRawAppBackendPath("f/my_script.ts"), "Should not detect standalone script"); + expect(isRawAppBackendPath("f/my_app__raw_app/backend/script.ts")).toBeTruthy(); + expect(!isRawAppBackendPath("f/my_app__raw_app/index.html")).toBeTruthy(); + expect(!isRawAppBackendPath("f/my_script.ts")).toBeTruthy(); } finally { // Restore original value setNonDottedPaths(wasNonDotted); } }); -Deno.test("Script fixture creates valid structure", () => { +test("Script fixture creates valid structure", () => { const pythonScript = createScriptFixture("test_script", "python3"); - assertEquals(pythonScript.contentFile.path, "test_script.py"); - assertEquals(pythonScript.metadataFile.path, "test_script.script.yaml"); - assertStringIncludes(pythonScript.contentFile.content, "def main()"); - assertStringIncludes(pythonScript.metadataFile.content, "summary:"); - assertStringIncludes(pythonScript.metadataFile.content, "kind: script"); + expect(pythonScript.contentFile.path).toEqual("test_script.py"); + expect(pythonScript.metadataFile.path).toEqual("test_script.script.yaml"); + expect(pythonScript.contentFile.content).toContain("def main()"); + expect(pythonScript.metadataFile.content).toContain("summary:"); + expect(pythonScript.metadataFile.content).toContain("kind: script"); }); -Deno.test("Flow fixture creates valid structure", () => { +test("Flow fixture creates valid structure", () => { const flow = createFlowFixture("test_flow"); - assertEquals(flow.metadata.path, "test_flow.flow/flow.yaml"); - assertEquals(flow.inlineScript.path, "test_flow.flow/a.ts"); - assertStringIncludes(flow.metadata.content, "summary:"); - assertStringIncludes(flow.metadata.content, "modules:"); - assertStringIncludes(flow.inlineScript.content, "export async function main"); + expect(flow.metadata.path).toEqual("test_flow.flow/flow.yaml"); + expect(flow.inlineScript.path).toEqual("test_flow.flow/a.ts"); + expect(flow.metadata.content).toContain("summary:"); + expect(flow.metadata.content).toContain("modules:"); + expect(flow.inlineScript.content).toContain("export async function main"); }); -Deno.test("App fixture creates valid structure", () => { +test("App fixture creates valid structure", () => { const app = createAppFixture("test_app"); - assertEquals(app.metadata.path, "test_app.app/app.yaml"); - assertStringIncludes(app.metadata.content, "summary:"); - assertStringIncludes(app.metadata.content, "grid:"); - assertStringIncludes(app.metadata.content, "policy:"); + expect(app.metadata.path).toEqual("test_app.app/app.yaml"); + expect(app.metadata.content).toContain("summary:"); + expect(app.metadata.content).toContain("grid:"); + expect(app.metadata.content).toContain("policy:"); }); -Deno.test("Raw app fixture creates valid structure", () => { +test("Raw app fixture creates valid structure", () => { const rawApp = createRawAppFixture("test_raw_app"); - assertEquals(rawApp.metadata.path, "test_raw_app.raw_app/raw_app.yaml"); - assertEquals(rawApp.indexHtml.path, "test_raw_app.raw_app/index.html"); - assertEquals(rawApp.indexJs.path, "test_raw_app.raw_app/index.js"); - assertStringIncludes(rawApp.metadata.content, "summary:"); - assertStringIncludes(rawApp.metadata.content, "runnables:"); + expect(rawApp.metadata.path).toEqual("test_raw_app.raw_app/raw_app.yaml"); + expect(rawApp.indexHtml.path).toEqual("test_raw_app.raw_app/index.html"); + expect(rawApp.indexJs.path).toEqual("test_raw_app.raw_app/index.js"); + expect(rawApp.metadata.content).toContain("summary:"); + expect(rawApp.metadata.content).toContain("runnables:"); }); -Deno.test("Resource fixture creates valid YAML", () => { +test("Resource fixture creates valid YAML", () => { const resource = createResourceFixture("postgres", "postgresql", { host: "localhost", port: 5432, }); - assertEquals(resource.path, "postgres.resource.yaml"); - assertStringIncludes(resource.content, 'resource_type: "postgresql"'); - assertStringIncludes(resource.content, "value:"); + expect(resource.path).toEqual("postgres.resource.yaml"); + expect(resource.content).toContain('resource_type: "postgresql"'); + expect(resource.content).toContain("value:"); }); -Deno.test("Variable fixture creates valid YAML", () => { +test("Variable fixture creates valid YAML", () => { const variable = createVariableFixture("my_var", "test_value", false); - assertEquals(variable.path, "my_var.variable.yaml"); - assertStringIncludes(variable.content, 'value: "test_value"'); - assertStringIncludes(variable.content, "is_secret: false"); + expect(variable.path).toEqual("my_var.variable.yaml"); + expect(variable.content).toContain('value: "test_value"'); + expect(variable.content).toContain("is_secret: false"); }); -Deno.test("Schedule fixture creates valid YAML", () => { +test("Schedule fixture creates valid YAML", () => { const schedule = createScheduleFixture("hourly_job", "u/admin/my_script", "0 * * * *"); - assertEquals(schedule.path, "hourly_job.schedule.yaml"); - assertStringIncludes(schedule.content, 'schedule: "0 * * * *"'); - assertStringIncludes(schedule.content, 'script_path: "u/admin/my_script"'); + expect(schedule.path).toEqual("hourly_job.schedule.yaml"); + expect(schedule.content).toContain('schedule: "0 * * * *"'); + expect(schedule.content).toContain('script_path: "u/admin/my_script"'); }); -Deno.test("HTTP trigger fixture creates valid YAML", () => { +test("HTTP trigger fixture creates valid YAML", () => { const trigger = createHttpTriggerFixture("webhook", "/api/webhook", "u/admin/handler"); - assertEquals(trigger.path, "webhook.http_trigger.yaml"); - assertStringIncludes(trigger.content, 'route_path: "/api/webhook"'); - assertStringIncludes(trigger.content, "http_method: post"); + expect(trigger.path).toEqual("webhook.http_trigger.yaml"); + expect(trigger.content).toContain('route_path: "/api/webhook"'); + expect(trigger.content).toContain("http_method: post"); }); -Deno.test("Folder fixture creates valid YAML", () => { +test("Folder fixture creates valid YAML", () => { const folder = createFolderFixture("my_folder"); - assertEquals(folder.path, "my_folder/folder.meta.yaml"); - assertStringIncludes(folder.content, 'display_name: "my_folder"'); + expect(folder.path).toEqual("my_folder/folder.meta.yaml"); + expect(folder.content).toContain('display_name: "my_folder"'); }); -Deno.test("User fixture creates valid YAML", () => { +test("User fixture creates valid YAML", () => { const user = createUserFixture("test_user", "test@example.com", true); - assertEquals(user.path, "test_user.user.yaml"); - assertStringIncludes(user.content, 'username: "test_user"'); - assertStringIncludes(user.content, 'email: "test@example.com"'); - assertStringIncludes(user.content, "is_admin: true"); + expect(user.path).toEqual("test_user.user.yaml"); + expect(user.content).toContain('username: "test_user"'); + expect(user.content).toContain('email: "test@example.com"'); + expect(user.content).toContain("is_admin: true"); }); -Deno.test("Group fixture creates valid YAML", () => { +test("Group fixture creates valid YAML", () => { const group = createGroupFixture("developers", ["user1", "user2"]); - assertEquals(group.path, "developers.group.yaml"); - assertStringIncludes(group.content, 'name: "developers"'); - assertStringIncludes(group.content, "- user1"); - assertStringIncludes(group.content, "- user2"); + expect(group.path).toEqual("developers.group.yaml"); + expect(group.content).toContain('name: "developers"'); + expect(group.content).toContain("- user1"); + expect(group.content).toContain("- user2"); }); -Deno.test("Local filesystem creation creates all expected files", async () => { +test("Local filesystem creation creates all expected files", async () => { const tempDir = await createTempDir(); try { @@ -831,41 +808,41 @@ Deno.test("Local filesystem creation creates all expected files", async () => { const files = await readDirRecursive(tempDir); // Check scripts exist - assert("f/scripts/python_script.py" in files, "Python script content should exist"); - assert("f/scripts/python_script.script.yaml" in files, "Python script metadata should exist"); - assert("f/scripts/deno_script.ts" in files, "Deno script content should exist"); - assert("f/scripts/bash_script.sh" in files, "Bash script content should exist"); - assert("f/scripts/go_script.go" in files, "Go script content should exist"); - assert("f/scripts/sql_script.sql" in files, "SQL script content should exist"); + expect("f/scripts/python_script.py" in files).toBeTruthy(); + expect("f/scripts/python_script.script.yaml" in files).toBeTruthy(); + expect("f/scripts/deno_script.ts" in files).toBeTruthy(); + expect("f/scripts/bash_script.sh" in files).toBeTruthy(); + expect("f/scripts/go_script.go" in files).toBeTruthy(); + expect("f/scripts/sql_script.sql" in files).toBeTruthy(); // Check flows exist - assert("f/flows/test_flow.flow/flow.yaml" in files, "Flow metadata should exist"); - assert("f/flows/test_flow.flow/a.ts" in files, "Flow inline script should exist"); + expect("f/flows/test_flow.flow/flow.yaml" in files).toBeTruthy(); + expect("f/flows/test_flow.flow/a.ts" in files).toBeTruthy(); // Check apps exist - assert("f/apps/test_app.app/app.yaml" in files, "App metadata should exist"); + expect("f/apps/test_app.app/app.yaml" in files).toBeTruthy(); // Check raw apps exist - assert("f/apps/test_raw_app.raw_app/raw_app.yaml" in files, "Raw app metadata should exist"); - assert("f/apps/test_raw_app.raw_app/index.html" in files, "Raw app HTML should exist"); - assert("f/apps/test_raw_app.raw_app/index.js" in files, "Raw app JS should exist"); + expect("f/apps/test_raw_app.raw_app/raw_app.yaml" in files).toBeTruthy(); + expect("f/apps/test_raw_app.raw_app/index.html" in files).toBeTruthy(); + expect("f/apps/test_raw_app.raw_app/index.js" in files).toBeTruthy(); // Check resources exist - assert("f/resources/postgres_db.resource.yaml" in files, "PostgreSQL resource should exist"); - assert("f/resources/api_config.resource.yaml" in files, "API config resource should exist"); + expect("f/resources/postgres_db.resource.yaml" in files).toBeTruthy(); + expect("f/resources/api_config.resource.yaml" in files).toBeTruthy(); // Check variables exist - assert("f/resources/config_value.variable.yaml" in files, "Config variable should exist"); - assert("f/resources/secret_key.variable.yaml" in files, "Secret variable should exist"); + expect("f/resources/config_value.variable.yaml" in files).toBeTruthy(); + expect("f/resources/secret_key.variable.yaml" in files).toBeTruthy(); // Check folder metadata - assert("f/folder.meta.yaml" in files, "Folder metadata should exist"); + expect("f/folder.meta.yaml" in files).toBeTruthy(); } finally { await cleanupTempDir(tempDir); } }); -Deno.test("Mock remote zip can be created and read", async () => { +test("Mock remote zip can be created and read", async () => { const items = { "test_script.py": 'def main():\n return "hello"', "test_script.script.json": '{"summary":"test","schema":{}}', @@ -877,26 +854,26 @@ Deno.test("Mock remote zip can be created and read", async () => { // Verify files exist in zip const scriptContent = await zip.file("test_script.py")?.async("text"); - assertEquals(scriptContent, 'def main():\n return "hello"'); + expect(scriptContent).toEqual('def main():\n return "hello"'); const flowContent = await zip.file("test_flow.flow.json")?.async("text"); - assertStringIncludes(flowContent!, '"summary":"flow"'); + expect(flowContent!).toContain('"summary":"flow"'); }); -Deno.test("readDirRecursive reads all files correctly", async () => { +test("readDirRecursive reads all files correctly", async () => { const tempDir = await createTempDir(); try { // Create a simple structure - await ensureDir(path.join(tempDir, "subdir")); - await Deno.writeTextFile(path.join(tempDir, "file1.txt"), "content1"); - await Deno.writeTextFile(path.join(tempDir, "subdir", "file2.txt"), "content2"); + await mkdir(path.join(tempDir, "subdir"), { recursive: true }); + await writeFile(path.join(tempDir, "file1.txt"), "content1", "utf-8"); + await writeFile(path.join(tempDir, "subdir", "file2.txt"), "content2", "utf-8"); const files = await readDirRecursive(tempDir); - assertEquals(files["file1.txt"], "content1"); - assertEquals(files["subdir/file2.txt"], "content2"); - assertEquals(Object.keys(files).length, 2); + expect(files["file1.txt"]).toEqual("content1"); + expect(files["subdir/file2.txt"]).toEqual("content2"); + expect(Object.keys(files).length).toEqual(2); } finally { await cleanupTempDir(tempDir); } @@ -906,67 +883,56 @@ Deno.test("readDirRecursive reads all files correctly", async () => { // Integration Tests (use withTestBackend for automated backend setup) // ============================================================================= -import { yamlParseFile } from "../deps.ts"; +import { yamlParseFile } from "../src/utils/yaml.ts"; import { withTestBackend } from "./test_backend.ts"; import { shouldSkipOnCI } from "./cargo_backend.ts"; -Deno.test({ - name: "Integration: Pull creates correct local structure", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Pull creates correct local structure", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, + "utf-8", ); // Run sync pull const result = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals( - result.code, - 0, - `Pull should succeed.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, - ); + expect(result.code).toEqual(0); // Verify files were created const files = await readDirRecursive(tempDir); const hasYamlFiles = Object.keys(files).some((f) => f.endsWith(".yaml") && f !== "wmill.yaml"); - assert(hasYamlFiles || Object.keys(files).length > 1, "Should have pulled files from server"); + expect(hasYamlFiles || Object.keys(files).length > 1).toBeTruthy(); }); - }, -}); + }); -Deno.test({ - name: "Integration: Push uploads local changes correctly", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Push uploads local changes correctly", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, + "utf-8", ); // Create a test script locally with a unique name // Path must have at least 2 segments after prefix (e.g., f/folder/name) const uniqueId = Date.now(); - await ensureDir(`${tempDir}/f/test`); + await mkdir(`${tempDir}/f/test`, { recursive: true }); const script = createScriptFixture(`f/test/push_script_${uniqueId}`, "deno"); - await Deno.writeTextFile(`${tempDir}/${script.contentFile.path}`, script.contentFile.content); - await Deno.writeTextFile(`${tempDir}/${script.metadataFile.path}`, script.metadataFile.content); + await writeFile(`${tempDir}/${script.contentFile.path}`, script.contentFile.content, "utf-8"); + await writeFile(`${tempDir}/${script.metadataFile.path}`, script.metadataFile.content, "utf-8"); // Run sync push with dry-run first (only push our test script, not everything) const dryRunResult = await backend.runCLICommand( @@ -974,16 +940,8 @@ excludes: [] tempDir, ); - assertEquals( - dryRunResult.code, - 0, - `Dry run should succeed.\nstdout: ${dryRunResult.stdout}\nstderr: ${dryRunResult.stderr}`, - ); - assertStringIncludes( - dryRunResult.stdout + dryRunResult.stderr, - `push_script_${uniqueId}`, - "Should detect the new script", - ); + expect(dryRunResult.code).toEqual(0); + expect(dryRunResult.stdout + dryRunResult.stderr).toContain(`push_script_${uniqueId}`); // Run actual push (only push our test script) const pushResult = await backend.runCLICommand( @@ -991,61 +949,41 @@ excludes: [] tempDir, ); - assertEquals( - pushResult.code, - 0, - `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, - ); + expect(pushResult.code).toEqual(0); }); - }, -}); + }); -Deno.test({ - name: "Integration: Pull then Push is idempotent", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Pull then Push is idempotent", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, + "utf-8", ); // Pull from remote const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals( - pullResult.code, - 0, - `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, - ); + expect(pullResult.code).toEqual(0); // Push back without changes (should be no-op) const pushResult = await backend.runCLICommand(["sync", "push", "--dry-run"], tempDir); - assertEquals(pushResult.code, 0, `Push dry-run should succeed: ${pushResult.stderr}`); + expect(pushResult.code).toEqual(0); // Should report 0 changes (check both stdout and stderr) const output = (pushResult.stdout + pushResult.stderr).toLowerCase(); - assert( - output.includes("0 change") || output.includes("no change") || output.includes("nothing"), - `Should have no changes after pull without modifications. Output: ${output}`, - ); + expect(output.includes("0 change") || output.includes("no change") || output.includes("nothing")).toBeTruthy(); }); - }, -}); + }); -Deno.test({ - name: "Integration: Include/exclude filters work correctly", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Include/exclude filters work correctly", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml with restrictive filters - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: @@ -1055,16 +993,13 @@ excludes: skipVariables: true skipResources: true `, + "utf-8", ); // Run sync pull const result = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals( - result.code, - 0, - `Pull should succeed.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, - ); + expect(result.code).toEqual(0); // Verify only scripts in f/scripts/ were pulled (if any exist) const files = await readDirRecursive(tempDir); @@ -1073,26 +1008,22 @@ skipResources: true const hasVariables = Object.keys(files).some((f) => f.includes(".variable.")); const hasResources = Object.keys(files).some((f) => f.includes(".resource.")); - assert(!hasVariables, "Should not have pulled variables (skipVariables: true)"); - assert(!hasResources, "Should not have pulled resources (skipResources: true)"); + expect(!hasVariables).toBeTruthy(); + expect(!hasResources).toBeTruthy(); }); - }, -}); + }); -Deno.test({ - name: "Integration: Flow folder structure is created correctly", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Flow folder structure is created correctly", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, + "utf-8", ); // Create a local flow with unique name @@ -1100,9 +1031,9 @@ excludes: [] const uniqueId = Date.now(); const flowName = `f/test/flow_${uniqueId}`; const flowFixture = createFlowFixture(flowName); - await ensureDir(`${tempDir}/f/test/flow_${uniqueId}${getFolderSuffix("flow")}`); + await mkdir(`${tempDir}/f/test/flow_${uniqueId}${getFolderSuffix("flow")}`, { recursive: true }); for (const file of Object.values(flowFixture)) { - await Deno.writeTextFile(`${tempDir}/${file.path}`, file.content); + await writeFile(`${tempDir}/${file.path}`, file.content, "utf-8"); } // Push the flow (only push our test flow, not everything) @@ -1112,14 +1043,10 @@ excludes: [] tempDir, ); - assertEquals( - pushResult.code, - 0, - `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, - ); + expect(pushResult.code).toEqual(0); // Pull back and verify structure is preserved - const tempDir2 = await Deno.makeTempDir({ prefix: "wmill_flow_verify_" }); + const tempDir2 = await mkdtemp(join(tmpdir(), "wmill_flow_verify_")); try { // Use template literal properly for the includes pattern const wmillConfig = `defaultTs: bun @@ -1127,56 +1054,45 @@ includes: - "f/test/flow_${uniqueId}*/**" excludes: [] `; - await Deno.writeTextFile(`${tempDir2}/wmill.yaml`, wmillConfig); + await writeFile(`${tempDir2}/wmill.yaml`, wmillConfig, "utf-8"); const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir2); - assertEquals( - pullResult.code, - 0, - `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, - ); + expect(pullResult.code).toEqual(0); // Verify flow folder structure const files = await readDirRecursive(tempDir2); const allFiles = Object.keys(files); const flowFiles = allFiles.filter((f) => f.includes(`flow_${uniqueId}`)); - assert(flowFiles.length > 0, `Should have pulled the flow. Files found: ${allFiles.join(", ")}`); - assert( - flowFiles.some((f) => f.includes(".flow/")), - "Flow should be in a .flow folder", - ); + expect(flowFiles.length > 0).toBeTruthy(); + expect(flowFiles.some((f) => f.includes(".flow/"))).toBeTruthy(); } finally { await cleanupTempDir(tempDir2); } }); - }, -}); + }); -Deno.test({ - name: "Integration: Raw app folder structure is handled correctly", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Raw app folder structure is handled correctly", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, + "utf-8", ); // Create a local raw app with unique name // Path must have at least 2 segments after prefix (e.g., f/folder/name) const uniqueId = Date.now(); const rawAppFixture = createRawAppFixture(`f/test/raw_app_${uniqueId}`); - await ensureDir(`${tempDir}/f/test/raw_app_${uniqueId}${getFolderSuffix("raw_app")}`); + await mkdir(`${tempDir}/f/test/raw_app_${uniqueId}${getFolderSuffix("raw_app")}`, { recursive: true }); for (const file of Object.values(rawAppFixture)) { - await Deno.writeTextFile(`${tempDir}/${file.path}`, file.content); + await writeFile(`${tempDir}/${file.path}`, file.content, "utf-8"); } // Push the raw app (only push our test raw app, not everything) @@ -1188,140 +1104,125 @@ excludes: [] // Note: This may fail if raw apps require specific validation // The test verifies the CLI handles the folder structure correctly if (pushResult.code === 0) { - assertStringIncludes( - pushResult.stdout + pushResult.stderr, - "", - "Push completed", - ); + expect(pushResult.stdout + pushResult.stderr).toContain(""); } }); - }, -}); + }); // ============================================================================= // nonDottedPaths Unit Tests // ============================================================================= -Deno.test("getFolderSuffixes returns correct suffixes for dotted paths (default)", () => { +test("getFolderSuffixes returns correct suffixes for dotted paths (default)", () => { setNonDottedPaths(false); const suffixes = getFolderSuffixes(); - assertEquals(suffixes.flow, ".flow"); - assertEquals(suffixes.app, ".app"); - assertEquals(suffixes.raw_app, ".raw_app"); + expect(suffixes.flow).toEqual(".flow"); + expect(suffixes.app).toEqual(".app"); + expect(suffixes.raw_app).toEqual(".raw_app"); }); -Deno.test("getFolderSuffixes returns correct suffixes for non-dotted paths", () => { +test("getFolderSuffixes returns correct suffixes for non-dotted paths", () => { setNonDottedPaths(true); const suffixes = getFolderSuffixes(); - assertEquals(suffixes.flow, "__flow"); - assertEquals(suffixes.app, "__app"); - assertEquals(suffixes.raw_app, "__raw_app"); + expect(suffixes.flow).toEqual("__flow"); + expect(suffixes.app).toEqual("__app"); + expect(suffixes.raw_app).toEqual("__raw_app"); setNonDottedPaths(false); // Reset }); -Deno.test("getFolderSuffix with nonDottedPaths returns dunder suffixes", () => { +test("getFolderSuffix with nonDottedPaths returns dunder suffixes", () => { setNonDottedPaths(true); - assertEquals(getFolderSuffix("flow"), "__flow"); - assertEquals(getFolderSuffix("app"), "__app"); - assertEquals(getFolderSuffix("raw_app"), "__raw_app"); + expect(getFolderSuffix("flow")).toEqual("__flow"); + expect(getFolderSuffix("app")).toEqual("__app"); + expect(getFolderSuffix("raw_app")).toEqual("__raw_app"); setNonDottedPaths(false); // Reset }); -Deno.test("buildFolderPath with nonDottedPaths creates correct paths", () => { +test("buildFolderPath with nonDottedPaths creates correct paths", () => { setNonDottedPaths(true); - assertEquals(buildFolderPath("my_flow", "flow"), "my_flow__flow"); - assertEquals(buildFolderPath("f/test/my_app", "app"), "f/test/my_app__app"); - assertEquals(buildFolderPath("u/admin/raw_app", "raw_app"), "u/admin/raw_app__raw_app"); + expect(buildFolderPath("my_flow", "flow")).toEqual("my_flow__flow"); + expect(buildFolderPath("f/test/my_app", "app")).toEqual("f/test/my_app__app"); + expect(buildFolderPath("u/admin/raw_app", "raw_app")).toEqual("u/admin/raw_app__raw_app"); setNonDottedPaths(false); // Reset }); -Deno.test("buildMetadataPath with nonDottedPaths creates correct paths", () => { +test("buildMetadataPath with nonDottedPaths creates correct paths", () => { setNonDottedPaths(true); - assertEquals( - buildMetadataPath("my_flow", "flow", "yaml"), - `my_flow__flow${SEP}flow.yaml` - ); - assertEquals( - buildMetadataPath(`f${SEP}test${SEP}my_app`, "app", "yaml"), - `f${SEP}test${SEP}my_app__app${SEP}app.yaml` - ); + // buildMetadataPath always uses forward slashes internally + expect(buildMetadataPath("my_flow", "flow", "yaml")).toEqual("my_flow__flow/flow.yaml"); + expect(buildMetadataPath("f/test/my_app", "app", "yaml")).toEqual("f/test/my_app__app/app.yaml"); setNonDottedPaths(false); // Reset }); -Deno.test("isFlowPath detects non-dotted paths when configured", () => { +test("isFlowPath detects non-dotted paths when configured", () => { // Default (dotted) paths setNonDottedPaths(false); - assert(isFlowPath(`f${SEP}test${SEP}my_flow.flow${SEP}flow.yaml`)); - assert(!isFlowPath(`f${SEP}test${SEP}my_flow__flow${SEP}flow.yaml`)); + expect(isFlowPath(`f${SEP}test${SEP}my_flow.flow${SEP}flow.yaml`)).toBeTruthy(); + expect(!isFlowPath(`f${SEP}test${SEP}my_flow__flow${SEP}flow.yaml`)).toBeTruthy(); // Non-dotted paths setNonDottedPaths(true); - assert(isFlowPath(`f${SEP}test${SEP}my_flow__flow${SEP}flow.yaml`)); - assert(!isFlowPath(`f${SEP}test${SEP}my_flow.flow${SEP}flow.yaml`)); + expect(isFlowPath(`f${SEP}test${SEP}my_flow__flow${SEP}flow.yaml`)).toBeTruthy(); + expect(!isFlowPath(`f${SEP}test${SEP}my_flow.flow${SEP}flow.yaml`)).toBeTruthy(); setNonDottedPaths(false); // Reset }); -Deno.test("isAppPath detects non-dotted paths when configured", () => { +test("isAppPath detects non-dotted paths when configured", () => { // Default (dotted) paths setNonDottedPaths(false); - assert(isAppPath(`f${SEP}test${SEP}my_app.app${SEP}app.yaml`)); - assert(!isAppPath(`f${SEP}test${SEP}my_app__app${SEP}app.yaml`)); + expect(isAppPath(`f${SEP}test${SEP}my_app.app${SEP}app.yaml`)).toBeTruthy(); + expect(!isAppPath(`f${SEP}test${SEP}my_app__app${SEP}app.yaml`)).toBeTruthy(); // Non-dotted paths setNonDottedPaths(true); - assert(isAppPath(`f${SEP}test${SEP}my_app__app${SEP}app.yaml`)); - assert(!isAppPath(`f${SEP}test${SEP}my_app.app${SEP}app.yaml`)); + expect(isAppPath(`f${SEP}test${SEP}my_app__app${SEP}app.yaml`)).toBeTruthy(); + expect(!isAppPath(`f${SEP}test${SEP}my_app.app${SEP}app.yaml`)).toBeTruthy(); setNonDottedPaths(false); // Reset }); -Deno.test("isRawAppPath detects non-dotted paths when configured", () => { +test("isRawAppPath detects non-dotted paths when configured", () => { // Default (dotted) paths setNonDottedPaths(false); - assert(isRawAppPath(`f${SEP}test${SEP}my_raw_app.raw_app${SEP}raw_app.yaml`)); - assert(!isRawAppPath(`f${SEP}test${SEP}my_raw_app__raw_app${SEP}raw_app.yaml`)); + expect(isRawAppPath(`f${SEP}test${SEP}my_raw_app.raw_app${SEP}raw_app.yaml`)).toBeTruthy(); + expect(!isRawAppPath(`f${SEP}test${SEP}my_raw_app__raw_app${SEP}raw_app.yaml`)).toBeTruthy(); // Non-dotted paths setNonDottedPaths(true); - assert(isRawAppPath(`f${SEP}test${SEP}my_raw_app__raw_app${SEP}raw_app.yaml`)); - assert(!isRawAppPath(`f${SEP}test${SEP}my_raw_app.raw_app${SEP}raw_app.yaml`)); + expect(isRawAppPath(`f${SEP}test${SEP}my_raw_app__raw_app${SEP}raw_app.yaml`)).toBeTruthy(); + expect(!isRawAppPath(`f${SEP}test${SEP}my_raw_app.raw_app${SEP}raw_app.yaml`)).toBeTruthy(); setNonDottedPaths(false); // Reset }); -Deno.test("extractResourceName works with non-dotted paths", () => { +test("extractResourceName works with non-dotted paths", () => { setNonDottedPaths(true); - assertEquals( - extractResourceName(`f${SEP}test${SEP}my_flow__flow${SEP}flow.yaml`, "flow"), - `f${SEP}test${SEP}my_flow` - ); - assertEquals( - extractResourceName(`f${SEP}test${SEP}my_app__app${SEP}app.yaml`, "app"), - `f${SEP}test${SEP}my_app` - ); + // extractResourceName normalizes separators to forward slashes + expect(extractResourceName(`f${SEP}test${SEP}my_flow__flow${SEP}flow.yaml`, "flow")).toEqual("f/test/my_flow"); + expect(extractResourceName(`f${SEP}test${SEP}my_app__app${SEP}app.yaml`, "app")).toEqual("f/test/my_app"); setNonDottedPaths(false); // Reset }); -Deno.test("hasFolderSuffix works with non-dotted paths", () => { +test("hasFolderSuffix works with non-dotted paths", () => { setNonDottedPaths(true); - assert(hasFolderSuffix("my_flow__flow", "flow")); - assert(!hasFolderSuffix("my_flow.flow", "flow")); + expect(hasFolderSuffix("my_flow__flow", "flow")).toBeTruthy(); + expect(!hasFolderSuffix("my_flow.flow", "flow")).toBeTruthy(); - assert(hasFolderSuffix("my_app__app", "app")); - assert(!hasFolderSuffix("my_app.app", "app")); + expect(hasFolderSuffix("my_app__app", "app")).toBeTruthy(); + expect(!hasFolderSuffix("my_app.app", "app")).toBeTruthy(); setNonDottedPaths(false); // Reset }); -Deno.test("setNonDottedPaths and getNonDottedPaths work correctly", () => { +test("setNonDottedPaths and getNonDottedPaths work correctly", () => { // Default should be false setNonDottedPaths(false); - assertEquals(getNonDottedPaths(), false); + expect(getNonDottedPaths()).toEqual(false); // Set to true setNonDottedPaths(true); - assertEquals(getNonDottedPaths(), true); + expect(getNonDottedPaths()).toEqual(true); // Set back to false setNonDottedPaths(false); - assertEquals(getNonDottedPaths(), false); + expect(getNonDottedPaths()).toEqual(false); }); // ============================================================================= @@ -1390,62 +1291,62 @@ policy: }; } -Deno.test("Flow fixture with nonDottedPaths creates __flow structure", () => { +test("Flow fixture with nonDottedPaths creates __flow structure", () => { setNonDottedPaths(true); const flow = createFlowFixtureWithCurrentConfig("test_flow"); - assertEquals(flow.metadata.path, "test_flow__flow/flow.yaml"); - assertEquals(flow.inlineScript.path, "test_flow__flow/a.ts"); - assertStringIncludes(flow.metadata.content, "summary:"); - assertStringIncludes(flow.metadata.content, "modules:"); + expect(flow.metadata.path).toEqual("test_flow__flow/flow.yaml"); + expect(flow.inlineScript.path).toEqual("test_flow__flow/a.ts"); + expect(flow.metadata.content).toContain("summary:"); + expect(flow.metadata.content).toContain("modules:"); setNonDottedPaths(false); // Reset }); -Deno.test("App fixture with nonDottedPaths creates __app structure", () => { +test("App fixture with nonDottedPaths creates __app structure", () => { setNonDottedPaths(true); const app = createAppFixtureWithCurrentConfig("test_app"); - assertEquals(app.metadata.path, "test_app__app/app.yaml"); - assertStringIncludes(app.metadata.content, "summary:"); - assertStringIncludes(app.metadata.content, "grid:"); + expect(app.metadata.path).toEqual("test_app__app/app.yaml"); + expect(app.metadata.content).toContain("summary:"); + expect(app.metadata.content).toContain("grid:"); setNonDottedPaths(false); // Reset }); -Deno.test("Local filesystem with nonDottedPaths creates correct folder structure", async () => { +test("Local filesystem with nonDottedPaths creates correct folder structure", async () => { setNonDottedPaths(true); const tempDir = await createTempDir(); try { // Create folder structure - await ensureDir(path.join(tempDir, "f/flows")); - await ensureDir(path.join(tempDir, "f/apps")); + await mkdir(path.join(tempDir, "f/flows"), { recursive: true }); + await mkdir(path.join(tempDir, "f/apps"), { recursive: true }); // Create flows with non-dotted paths const flowFixture = createFlowFixtureWithCurrentConfig("f/flows/test_flow"); - await ensureDir(path.join(tempDir, `f/flows/test_flow${getFolderSuffix("flow")}`)); + await mkdir(path.join(tempDir, `f/flows/test_flow${getFolderSuffix("flow")}`), { recursive: true }); for (const file of Object.values(flowFixture)) { - await Deno.writeTextFile(path.join(tempDir, file.path), file.content); + await writeFile(path.join(tempDir, file.path), file.content, "utf-8"); } // Create apps with non-dotted paths const appFixture = createAppFixtureWithCurrentConfig("f/apps/test_app"); - await ensureDir(path.join(tempDir, `f/apps/test_app${getFolderSuffix("app")}`)); + await mkdir(path.join(tempDir, `f/apps/test_app${getFolderSuffix("app")}`), { recursive: true }); for (const file of Object.values(appFixture)) { - await Deno.writeTextFile(path.join(tempDir, file.path), file.content); + await writeFile(path.join(tempDir, file.path), file.content, "utf-8"); } const files = await readDirRecursive(tempDir); // Check flows exist with __flow suffix - assert("f/flows/test_flow__flow/flow.yaml" in files, "Flow metadata should exist with __flow suffix"); - assert("f/flows/test_flow__flow/a.ts" in files, "Flow inline script should exist with __flow suffix"); + expect("f/flows/test_flow__flow/flow.yaml" in files).toBeTruthy(); + expect("f/flows/test_flow__flow/a.ts" in files).toBeTruthy(); // Check apps exist with __app suffix - assert("f/apps/test_app__app/app.yaml" in files, "App metadata should exist with __app suffix"); + expect("f/apps/test_app__app/app.yaml" in files).toBeTruthy(); // Verify old-style paths don't exist - assert(!("f/flows/test_flow.flow/flow.yaml" in files), "Old .flow suffix should not exist"); - assert(!("f/apps/test_app.app/app.yaml" in files), "Old .app suffix should not exist"); + expect(!("f/flows/test_flow.flow/flow.yaml" in files)).toBeTruthy(); + expect(!("f/apps/test_app.app/app.yaml" in files)).toBeTruthy(); } finally { await cleanupTempDir(tempDir); setNonDottedPaths(false); // Reset @@ -1456,14 +1357,10 @@ Deno.test("Local filesystem with nonDottedPaths creates correct folder structure // nonDottedPaths Integration Tests // ============================================================================= -Deno.test({ - name: "Integration: wmill.yaml with nonDottedPaths is read correctly", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: wmill.yaml with nonDottedPaths is read correctly", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml with nonDottedPaths option - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun nonDottedPaths: true @@ -1471,6 +1368,7 @@ includes: - "f/**" excludes: [] `, + "utf-8", ); // Create a test script with non-dotted flow folder @@ -1478,9 +1376,9 @@ excludes: [] const uniqueId = Date.now(); const flowName = `f/test/nondot_flow_${uniqueId}`; const flowFixture = createFlowFixtureWithCurrentConfig(flowName); - await ensureDir(`${tempDir}/f/test/nondot_flow_${uniqueId}${getFolderSuffix("flow")}`); + await mkdir(`${tempDir}/f/test/nondot_flow_${uniqueId}${getFolderSuffix("flow")}`, { recursive: true }); for (const file of Object.values(flowFixture)) { - await Deno.writeTextFile(`${tempDir}/${file.path}`, file.content); + await writeFile(`${tempDir}/${file.path}`, file.content, "utf-8"); } setNonDottedPaths(false); // Reset @@ -1490,23 +1388,14 @@ excludes: [] tempDir, ); - assertEquals( - dryRunResult.code, - 0, - `Dry run should succeed with nonDottedPaths config.\nstdout: ${dryRunResult.stdout}\nstderr: ${dryRunResult.stderr}`, - ); + expect(dryRunResult.code).toEqual(0); }); - }, -}); + }); -Deno.test({ - name: "Integration: Pull then Push with nonDottedPaths is idempotent", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Pull then Push with nonDottedPaths is idempotent", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml with nonDottedPaths enabled - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun nonDottedPaths: true @@ -1514,15 +1403,12 @@ includes: - "**" excludes: [] `, + "utf-8", ); // Pull from remote with nonDottedPaths enabled const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals( - pullResult.code, - 0, - `Pull should succeed with nonDottedPaths.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, - ); + expect(pullResult.code).toEqual(0); // Verify that pulled files use __flow/__app/__raw_app suffixes const filesAfterPull = await readDirRecursive(tempDir); @@ -1536,40 +1422,26 @@ excludes: [] // Only check if there are actually flows/apps in the workspace // If there are flows, they should use __flow not .flow if (flowFiles.length > 0 || dottedFlowFiles.length > 0) { - assert( - dottedFlowFiles.length === 0, - `Flows should use __flow suffix with nonDottedPaths, found .flow files: ${dottedFlowFiles.join(", ")}`, - ); + expect(dottedFlowFiles.length === 0).toBeTruthy(); } if (appFiles.length > 0 || dottedAppFiles.length > 0) { - assert( - dottedAppFiles.length === 0, - `Apps should use __app suffix with nonDottedPaths, found .app files: ${dottedAppFiles.join(", ")}`, - ); + expect(dottedAppFiles.length === 0).toBeTruthy(); } // Push back without changes (should be no-op / idempotent) const pushResult = await backend.runCLICommand(["sync", "push", "--dry-run"], tempDir); - assertEquals(pushResult.code, 0, `Push dry-run should succeed: ${pushResult.stderr}`); + expect(pushResult.code).toEqual(0); // Should report 0 changes (check both stdout and stderr) const output = (pushResult.stdout + pushResult.stderr).toLowerCase(); - assert( - output.includes("0 change") || output.includes("no change") || output.includes("nothing"), - `Should have no changes after pull with nonDottedPaths without modifications. Output: ${output}`, - ); + expect(output.includes("0 change") || output.includes("no change") || output.includes("nothing")).toBeTruthy(); }); - }, -}); + }); -Deno.test({ - name: "Integration: Push flow with nonDottedPaths creates __flow structure on server", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Push flow with nonDottedPaths creates __flow structure on server", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml with nonDottedPaths enabled - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun nonDottedPaths: true @@ -1577,6 +1449,7 @@ includes: - "**" excludes: [] `, + "utf-8", ); // Create a local flow with __flow suffix @@ -1584,9 +1457,9 @@ excludes: [] const uniqueId = Date.now(); const flowName = `f/test/nondot_idem_flow_${uniqueId}`; const flowFixture = createFlowFixtureWithCurrentConfig(flowName); - await ensureDir(`${tempDir}/f/test/nondot_idem_flow_${uniqueId}${getFolderSuffix("flow")}`); + await mkdir(`${tempDir}/f/test/nondot_idem_flow_${uniqueId}${getFolderSuffix("flow")}`, { recursive: true }); for (const file of Object.values(flowFixture)) { - await Deno.writeTextFile(`${tempDir}/${file.path}`, file.content); + await writeFile(`${tempDir}/${file.path}`, file.content, "utf-8"); } setNonDottedPaths(false); // Reset global state @@ -1596,11 +1469,7 @@ excludes: [] tempDir, ); - assertEquals( - pushResult.code, - 0, - `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, - ); + expect(pushResult.code).toEqual(0); // Pull back to same directory to verify round-trip (idempotency) const pullResult = await backend.runCLICommand( @@ -1608,26 +1477,16 @@ excludes: [] tempDir, ); - assertEquals( - pullResult.code, - 0, - `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, - ); + expect(pullResult.code).toEqual(0); // Verify flow still has __flow suffix after round-trip const filesAfterPull = await readDirRecursive(tempDir); const allFiles = Object.keys(filesAfterPull); const flowFiles = allFiles.filter((f) => f.includes(`nondot_idem_flow_${uniqueId}`)); - assert(flowFiles.length > 0, `Should have the flow files after pull. Files found: ${allFiles.join(", ")}`); - assert( - flowFiles.some((f) => f.includes("__flow/")), - `Flow should be in a __flow folder with nonDottedPaths. Found: ${flowFiles.join(", ")}`, - ); - assert( - !flowFiles.some((f) => f.includes(".flow/")), - `Flow should NOT use .flow suffix with nonDottedPaths. Found: ${flowFiles.join(", ")}`, - ); + expect(flowFiles.length > 0).toBeTruthy(); + expect(flowFiles.some((f) => f.includes("__flow/"))).toBeTruthy(); + expect(!flowFiles.some((f) => f.includes(".flow/"))).toBeTruthy(); // Push again (should be idempotent - no changes) const push2 = await backend.runCLICommand( @@ -1635,25 +1494,17 @@ excludes: [] tempDir, ); - assertEquals(push2.code, 0, `Second push dry-run should succeed: ${push2.stderr}`); + expect(push2.code).toEqual(0); const output = (push2.stdout + push2.stderr).toLowerCase(); - assert( - output.includes("0 change") || output.includes("no change") || output.includes("nothing"), - `Should have no changes after push-pull cycle for flow. Output: ${output}`, - ); + expect(output.includes("0 change") || output.includes("no change") || output.includes("nothing")).toBeTruthy(); }); - }, -}); + }); -Deno.test({ - name: "Integration: Multiple pull/push cycles with nonDottedPaths remain idempotent", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Multiple pull/push cycles with nonDottedPaths remain idempotent", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml with nonDottedPaths enabled - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun nonDottedPaths: true @@ -1661,56 +1512,46 @@ includes: - "**" excludes: [] `, + "utf-8", ); // First pull const pull1 = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pull1.code, 0, `First pull should succeed: ${pull1.stderr}`); + expect(pull1.code).toEqual(0); // First push (should be no-op) const push1 = await backend.runCLICommand(["sync", "push", "--dry-run"], tempDir); - assertEquals(push1.code, 0, `First push dry-run should succeed: ${push1.stderr}`); + expect(push1.code).toEqual(0); // Second pull (should have no changes) const pull2 = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pull2.code, 0, `Second pull should succeed: ${pull2.stderr}`); + expect(pull2.code).toEqual(0); // Second push (should still be no-op) const push2 = await backend.runCLICommand(["sync", "push", "--dry-run"], tempDir); - assertEquals(push2.code, 0, `Second push dry-run should succeed: ${push2.stderr}`); + expect(push2.code).toEqual(0); // Verify no changes after multiple cycles const output = (push2.stdout + push2.stderr).toLowerCase(); - assert( - output.includes("0 change") || output.includes("no change") || output.includes("nothing"), - `Should have no changes after multiple pull/push cycles with nonDottedPaths. Output: ${output}`, - ); + expect(output.includes("0 change") || output.includes("no change") || output.includes("nothing")).toBeTruthy(); // Third pull to verify consistency const pull3 = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); - assertEquals(pull3.code, 0, `Third pull should succeed: ${pull3.stderr}`); + expect(pull3.code).toEqual(0); // Final push check const push3 = await backend.runCLICommand(["sync", "push", "--dry-run"], tempDir); - assertEquals(push3.code, 0, `Final push dry-run should succeed: ${push3.stderr}`); + expect(push3.code).toEqual(0); const finalOutput = (push3.stdout + push3.stderr).toLowerCase(); - assert( - finalOutput.includes("0 change") || finalOutput.includes("no change") || finalOutput.includes("nothing"), - `Should still have no changes after 3 cycles. Output: ${finalOutput}`, - ); + expect(finalOutput.includes("0 change") || finalOutput.includes("no change") || finalOutput.includes("nothing")).toBeTruthy(); }); - }, -}); + }); -Deno.test({ - name: "Integration: App with nonDottedPaths creates __app structure and is idempotent", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: App with nonDottedPaths creates __app structure and is idempotent", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml with nonDottedPaths enabled - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun nonDottedPaths: true @@ -1718,6 +1559,7 @@ includes: - "**" excludes: [] `, + "utf-8", ); // Create a local app with __app suffix @@ -1725,9 +1567,9 @@ excludes: [] const uniqueId = Date.now(); const appName = `f/test/nondot_app_${uniqueId}`; const appFixture = createAppFixtureWithCurrentConfig(appName); - await ensureDir(`${tempDir}/f/test/nondot_app_${uniqueId}${getFolderSuffix("app")}`); + await mkdir(`${tempDir}/f/test/nondot_app_${uniqueId}${getFolderSuffix("app")}`, { recursive: true }); for (const file of Object.values(appFixture)) { - await Deno.writeTextFile(`${tempDir}/${file.path}`, file.content); + await writeFile(`${tempDir}/${file.path}`, file.content, "utf-8"); } setNonDottedPaths(false); // Reset global state @@ -1737,11 +1579,7 @@ excludes: [] tempDir, ); - assertEquals( - pushResult.code, - 0, - `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, - ); + expect(pushResult.code).toEqual(0); // Pull back to same directory const pullResult = await backend.runCLICommand( @@ -1749,21 +1587,14 @@ excludes: [] tempDir, ); - assertEquals( - pullResult.code, - 0, - `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, - ); + expect(pullResult.code).toEqual(0); // Verify app structure uses __app const files = await readDirRecursive(tempDir); const appFiles = Object.keys(files).filter((f) => f.includes(`nondot_app_${uniqueId}`)); - assert(appFiles.length > 0, `Should have the app files. Found: ${Object.keys(files).join(", ")}`); - assert( - appFiles.some((f) => f.includes("__app/")), - `App should be in a __app folder with nonDottedPaths. Found: ${appFiles.join(", ")}`, - ); + expect(appFiles.length > 0).toBeTruthy(); + expect(appFiles.some((f) => f.includes("__app/"))).toBeTruthy(); // Push again (should be idempotent) const push2 = await backend.runCLICommand( @@ -1771,16 +1602,12 @@ excludes: [] tempDir, ); - assertEquals(push2.code, 0, `Second push dry-run should succeed: ${push2.stderr}`); + expect(push2.code).toEqual(0); const output = (push2.stdout + push2.stderr).toLowerCase(); - assert( - output.includes("0 change") || output.includes("no change") || output.includes("nothing"), - `Should have no changes after push-pull cycle for app. Output: ${output}`, - ); + expect(output.includes("0 change") || output.includes("no change") || output.includes("nothing")).toBeTruthy(); }); - }, -}); + }); /** * Creates a mock raw_app file structure using the current global nonDottedPaths setting @@ -1814,14 +1641,10 @@ runnables: }; } -Deno.test({ - name: "Integration: Raw app with nonDottedPaths creates __raw_app structure", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Raw app with nonDottedPaths creates __raw_app structure", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml with nonDottedPaths enabled - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun nonDottedPaths: true @@ -1829,6 +1652,7 @@ includes: - "**" excludes: [] `, + "utf-8", ); // Create a local raw app with __raw_app suffix @@ -1836,9 +1660,9 @@ excludes: [] const uniqueId = Date.now(); const rawAppName = `f/test/nondot_rawapp_${uniqueId}`; const rawAppFixture = createRawAppFixtureWithCurrentConfig(rawAppName); - await ensureDir(`${tempDir}/f/test/nondot_rawapp_${uniqueId}${getFolderSuffix("raw_app")}`); + await mkdir(`${tempDir}/f/test/nondot_rawapp_${uniqueId}${getFolderSuffix("raw_app")}`, { recursive: true }); for (const file of Object.values(rawAppFixture)) { - await Deno.writeTextFile(`${tempDir}/${file.path}`, file.content); + await writeFile(`${tempDir}/${file.path}`, file.content, "utf-8"); } setNonDottedPaths(false); // Reset global state @@ -1846,15 +1670,9 @@ excludes: [] const files = await readDirRecursive(tempDir); const rawAppFiles = Object.keys(files).filter((f) => f.includes(`nondot_rawapp_${uniqueId}`)); - assert(rawAppFiles.length > 0, `Should have created raw app files. Found: ${Object.keys(files).join(", ")}`); - assert( - rawAppFiles.some((f) => f.includes("__raw_app/")), - `Raw app should be in a __raw_app folder with nonDottedPaths. Found: ${rawAppFiles.join(", ")}`, - ); - assert( - !rawAppFiles.some((f) => f.includes(".raw_app/")), - `Raw app should NOT use .raw_app suffix with nonDottedPaths. Found: ${rawAppFiles.join(", ")}`, - ); + expect(rawAppFiles.length > 0).toBeTruthy(); + expect(rawAppFiles.some((f) => f.includes("__raw_app/"))).toBeTruthy(); + expect(!rawAppFiles.some((f) => f.includes(".raw_app/"))).toBeTruthy(); // Push the raw app (may fail if raw apps require specific validation) const pushResult = await backend.runCLICommand( @@ -1871,20 +1689,15 @@ excludes: [] tempDir, ); - assertEquals(push2.code, 0, `Second push dry-run should succeed: ${push2.stderr}`); + expect(push2.code).toEqual(0); } }); - }, -}); + }); -Deno.test({ - name: "Integration: Mixed scripts and flows with nonDottedPaths are idempotent", - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test("Integration: Mixed scripts and flows with nonDottedPaths are idempotent", async () => { await withTestBackend(async (backend, tempDir) => { // Create wmill.yaml with nonDottedPaths enabled - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun nonDottedPaths: true @@ -1892,23 +1705,24 @@ includes: - "**" excludes: [] `, + "utf-8", ); const uniqueId = Date.now(); - await ensureDir(`${tempDir}/f/test`); + await mkdir(`${tempDir}/f/test`, { recursive: true }); // Create a script (scripts don't use folder suffixes, so they're unaffected) const script = createScriptFixture(`f/test/mixed_script_${uniqueId}`, "deno"); - await Deno.writeTextFile(`${tempDir}/${script.contentFile.path}`, script.contentFile.content); - await Deno.writeTextFile(`${tempDir}/${script.metadataFile.path}`, script.metadataFile.content); + await writeFile(`${tempDir}/${script.contentFile.path}`, script.contentFile.content, "utf-8"); + await writeFile(`${tempDir}/${script.metadataFile.path}`, script.metadataFile.content, "utf-8"); // Create a flow with __flow suffix setNonDottedPaths(true); const flowName = `f/test/mixed_flow_${uniqueId}`; const flowFixture = createFlowFixtureWithCurrentConfig(flowName); - await ensureDir(`${tempDir}/f/test/mixed_flow_${uniqueId}${getFolderSuffix("flow")}`); + await mkdir(`${tempDir}/f/test/mixed_flow_${uniqueId}${getFolderSuffix("flow")}`, { recursive: true }); for (const file of Object.values(flowFixture)) { - await Deno.writeTextFile(`${tempDir}/${file.path}`, file.content); + await writeFile(`${tempDir}/${file.path}`, file.content, "utf-8"); } setNonDottedPaths(false); // Reset global state @@ -1918,11 +1732,7 @@ excludes: [] tempDir, ); - assertEquals( - pushResult.code, - 0, - `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, - ); + expect(pushResult.code).toEqual(0); // Pull back const pullResult = await backend.runCLICommand( @@ -1930,11 +1740,7 @@ excludes: [] tempDir, ); - assertEquals( - pullResult.code, - 0, - `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, - ); + expect(pullResult.code).toEqual(0); // Verify idempotency const push2 = await backend.runCLICommand( @@ -1942,130 +1748,99 @@ excludes: [] tempDir, ); - assertEquals(push2.code, 0, `Second push dry-run should succeed: ${push2.stderr}`); + expect(push2.code).toEqual(0); const output = (push2.stdout + push2.stderr).toLowerCase(); - assert( - output.includes("0 change") || output.includes("no change") || output.includes("nothing"), - `Should have no changes after push-pull cycle for mixed content. Output: ${output}`, - ); + expect(output.includes("0 change") || output.includes("no change") || output.includes("nothing")).toBeTruthy(); }); - }, -}); + }); // ============================================================================= // ws_error_handler_muted Persistence Tests // ============================================================================= -Deno.test({ - name: "Integration: Script ws_error_handler_muted is persisted through push/pull", - ignore: shouldSkipOnCI(), // Requires EE features - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test.skipIf(shouldSkipOnCI())("Integration: Script ws_error_handler_muted is persisted through push/pull", async () => { await withTestBackend(async (backend, tempDir) => { - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, + "utf-8", ); const uniqueId = Date.now(); - await ensureDir(`${tempDir}/f/test`); + await mkdir(`${tempDir}/f/test`, { recursive: true }); // Create a script with ws_error_handler_muted: true const scriptName = `f/test/muted_script_${uniqueId}`; const script = createScriptFixture(scriptName, "deno"); - await Deno.writeTextFile(`${tempDir}/${script.contentFile.path}`, script.contentFile.content); + await writeFile(`${tempDir}/${script.contentFile.path}`, script.contentFile.content, "utf-8"); // Add ws_error_handler_muted to the metadata const metadataWithMuted = script.metadataFile.content + `ws_error_handler_muted: true\n`; - await Deno.writeTextFile(`${tempDir}/${script.metadataFile.path}`, metadataWithMuted); + await writeFile(`${tempDir}/${script.metadataFile.path}`, metadataWithMuted, "utf-8"); // Push const pushResult = await backend.runCLICommand( ["sync", "push", "--yes", "--includes", `f/test/muted_script_${uniqueId}**`], tempDir, ); - assertEquals( - pushResult.code, - 0, - `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, - ); + expect(pushResult.code).toEqual(0); // Verify via API that ws_error_handler_muted was persisted const apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/scripts/get/p/${scriptName}`, ); - assertEquals(apiResp.status, 200, "API should return the script"); + expect(apiResp.status).toEqual(200); const scriptData = await apiResp.json(); - assertEquals( - scriptData.ws_error_handler_muted, - true, - "API should return ws_error_handler_muted: true for the pushed script", - ); + expect(scriptData.ws_error_handler_muted).toEqual(true); // Pull into a fresh directory and verify the field round-trips - const pullDir = await Deno.makeTempDir({ prefix: "wmill_muted_script_pull_" }); + const pullDir = await mkdtemp(join(tmpdir(), "wmill_muted_script_pull_")); try { - await Deno.writeTextFile( + await writeFile( `${pullDir}/wmill.yaml`, `defaultTs: bun includes: - "f/test/muted_script_${uniqueId}**" excludes: [] `, + "utf-8", ); const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], pullDir); - assertEquals( - pullResult.code, - 0, - `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, - ); + expect(pullResult.code).toEqual(0); // Verify ws_error_handler_muted is in the pulled metadata - const pulledMetadata = await Deno.readTextFile(`${pullDir}/${script.metadataFile.path}`); - assertStringIncludes( - pulledMetadata, - "ws_error_handler_muted: true", - "Pulled script metadata should contain ws_error_handler_muted: true", - ); + const pulledMetadata = await readFile(`${pullDir}/${script.metadataFile.path}`, "utf-8"); + expect(pulledMetadata).toContain("ws_error_handler_muted: true"); // Verify push from pulled dir is idempotent (no changes) const push2 = await backend.runCLICommand( ["sync", "push", "--dry-run", "--includes", `f/test/muted_script_${uniqueId}**`], pullDir, ); - assertEquals(push2.code, 0, `Second push dry-run should succeed: ${push2.stderr}`); + expect(push2.code).toEqual(0); const output = (push2.stdout + push2.stderr).toLowerCase(); - assert( - output.includes("0 change") || output.includes("no change") || output.includes("nothing"), - `Should have no changes after push-pull cycle for script with ws_error_handler_muted. Output: ${output}`, - ); + expect(output.includes("0 change") || output.includes("no change") || output.includes("nothing")).toBeTruthy(); } finally { - await Deno.remove(pullDir, { recursive: true }).catch(() => {}); + await rm(pullDir, { recursive: true }).catch(() => {}); } }); - }, -}); + }); -Deno.test({ - name: "Integration: Flow ws_error_handler_muted is persisted through push/pull", - ignore: shouldSkipOnCI(), // Requires EE features - sanitizeResources: false, - sanitizeOps: false, - async fn() { +test.skipIf(shouldSkipOnCI())("Integration: Flow ws_error_handler_muted is persisted through push/pull", async () => { await withTestBackend(async (backend, tempDir) => { - await Deno.writeTextFile( + await writeFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, + "utf-8", ); const uniqueId = Date.now(); @@ -2073,14 +1848,14 @@ excludes: [] const flowFixture = createFlowFixture(flowName); // Create flow directory and files - await ensureDir(`${tempDir}/f/test/muted_flow_${uniqueId}${getFolderSuffix("flow")}`); + await mkdir(`${tempDir}/f/test/muted_flow_${uniqueId}${getFolderSuffix("flow")}`, { recursive: true }); for (const [key, file] of Object.entries(flowFixture)) { if (key === "metadata") { // Add ws_error_handler_muted to flow metadata const contentWithMuted = file.content + `ws_error_handler_muted: true\n`; - await Deno.writeTextFile(`${tempDir}/${file.path}`, contentWithMuted); + await writeFile(`${tempDir}/${file.path}`, contentWithMuted, "utf-8"); } else { - await Deno.writeTextFile(`${tempDir}/${file.path}`, file.content); + await writeFile(`${tempDir}/${file.path}`, file.content, "utf-8"); } } @@ -2089,75 +1864,467 @@ excludes: [] ["sync", "push", "--yes", "--includes", `f/test/muted_flow_${uniqueId}*/**`], tempDir, ); - assertEquals( - pushResult.code, - 0, - `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, - ); + expect(pushResult.code).toEqual(0); // Verify via API that ws_error_handler_muted was persisted const apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/flows/get/${flowName}`, ); - assertEquals(apiResp.status, 200, "API should return the flow"); + expect(apiResp.status).toEqual(200); const flowData = await apiResp.json(); - assertEquals( - flowData.ws_error_handler_muted, - true, - "API should return ws_error_handler_muted: true for the pushed flow", - ); + expect(flowData.ws_error_handler_muted).toEqual(true); // Pull into a fresh directory and verify the field round-trips - const pullDir = await Deno.makeTempDir({ prefix: "wmill_muted_flow_pull_" }); + const pullDir = await mkdtemp(join(tmpdir(), "wmill_muted_flow_pull_")); try { - await Deno.writeTextFile( + await writeFile( `${pullDir}/wmill.yaml`, `defaultTs: bun includes: - "f/test/muted_flow_${uniqueId}*/**" excludes: [] `, + "utf-8", ); const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], pullDir); - assertEquals( - pullResult.code, - 0, - `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, - ); + expect(pullResult.code).toEqual(0); // Verify ws_error_handler_muted is in the pulled flow.yaml const flowYamlPath = `${pullDir}/${flowFixture.metadata.path}`; - const pulledFlowYaml = await Deno.readTextFile(flowYamlPath); - assertStringIncludes( - pulledFlowYaml, - "ws_error_handler_muted: true", - "Pulled flow.yaml should contain ws_error_handler_muted: true", - ); + const pulledFlowYaml = await readFile(flowYamlPath, "utf-8"); + expect(pulledFlowYaml).toContain("ws_error_handler_muted: true"); // Parse the YAML to confirm it's a proper boolean value // deno-lint-ignore no-explicit-any const parsed = await yamlParseFile(flowYamlPath) as any; - assertEquals( - parsed.ws_error_handler_muted, - true, - "ws_error_handler_muted should be boolean true in parsed flow YAML", - ); + expect(parsed.ws_error_handler_muted).toEqual(true); // Verify push from pulled dir is idempotent (no changes) const push2 = await backend.runCLICommand( ["sync", "push", "--dry-run", "--includes", `f/test/muted_flow_${uniqueId}*/**`], pullDir, ); - assertEquals(push2.code, 0, `Second push dry-run should succeed: ${push2.stderr}`); + expect(push2.code).toEqual(0); const output = (push2.stdout + push2.stderr).toLowerCase(); - assert( - output.includes("0 change") || output.includes("no change") || output.includes("nothing"), - `Should have no changes after push-pull cycle for flow with ws_error_handler_muted. Output: ${output}`, - ); + expect(output.includes("0 change") || output.includes("no change") || output.includes("nothing")).toBeTruthy(); } finally { - await Deno.remove(pullDir, { recursive: true }).catch(() => {}); + await rm(pullDir, { recursive: true }).catch(() => {}); } }); - }, + }); + +// ============================================================================= +// Sync tests for groups, settings, resource types, schedules, and HTTP triggers +// ============================================================================= + +import type { TestBackend } from "./test_backend.ts"; + +/** Create a script on the remote via API */ +async function createRemoteScript( + backend: TestBackend, + scriptPath: string, + content: string = 'export async function main() { return "hello"; }' +): Promise { + const resp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: scriptPath, + content, + language: "bun", + summary: "Test script", + description: "Created by integration test", + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + required: [], + }, + }), + } + ); + expect(resp.status).toBeLessThan(300); + await resp.text(); +} + +/** Write a standard wmill.yaml with the given extra flags */ +async function writeWmillYaml( + tempDir: string, + extraFlags: string = "" +): Promise { + await writeFile( + `${tempDir}/wmill.yaml`, + `defaultTs: bun +includes: + - "**" +excludes: [] +${extraFlags}`, + "utf-8" + ); +} + +/** Recursively list all files relative to baseDir, returning forward-slash paths */ +async function listFilesRecursive( + dir: string, + baseDir: string = dir +): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + const files: string[] = []; + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + const relativePath = fullPath + .substring(baseDir.length + 1) + .replaceAll("\\", "/"); + if (entry.isDirectory()) { + files.push(...(await listFilesRecursive(fullPath, baseDir))); + } else { + files.push(relativePath); + } + } + return files; +} + +describe("group sync", () => { + test("Integration: Group pull/push round-trip", async () => { + await withTestBackend(async (backend, tempDir) => { + await writeWmillYaml(tempDir, "includeGroups: true"); + + // Pull with --include-groups + const pullResult = await backend.runCLICommand( + ["sync", "pull", "--yes", "--include-groups"], + tempDir + ); + expect(pullResult.code).toEqual(0); + + // Verify group file was created (seedTestData creates test_group) + const files = await listFilesRecursive(tempDir); + const groupFiles = files.filter((f) => f.endsWith(".group.yaml")); + expect(groupFiles.length).toBeGreaterThan(0); + + const testGroupFile = groupFiles.find((f) => f.includes("test_group")); + expect(testGroupFile).toBeDefined(); + + // Read the group file and modify + const groupContent = await readFile(`${tempDir}/${testGroupFile!}`, "utf-8"); + expect(groupContent).toContain("summary"); + + const modifiedContent = groupContent.replace( + /summary:.*/, + 'summary: "Modified group summary from test"' + ); + await writeFile(`${tempDir}/${testGroupFile!}`, modifiedContent, "utf-8"); + + // Push the modification + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--include-groups"], + tempDir + ); + expect(pushResult.code).toEqual(0); + + // Verify via API + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/groups/get/test_group` + ); + expect(apiResp.status).toEqual(200); + const groupData = await apiResp.json(); + expect(groupData.summary).toEqual("Modified group summary from test"); + }); + }); +}); + +describe("settings sync", () => { + test("Integration: Settings pull/push round-trip", async () => { + await withTestBackend(async (backend, tempDir) => { + await writeWmillYaml(tempDir, "includeSettings: true"); + + // Pull with --include-settings + const pullResult = await backend.runCLICommand( + ["sync", "pull", "--yes", "--include-settings"], + tempDir + ); + expect(pullResult.code).toEqual(0); + + // Verify settings.yaml exists + const files = await listFilesRecursive(tempDir); + expect(files).toContain("settings.yaml"); + + // Read and modify a safe setting (webhook URL) + const settingsContent = await readFile(`${tempDir}/settings.yaml`, "utf-8"); + + let modifiedSettings: string; + if (settingsContent.includes("webhook:")) { + modifiedSettings = settingsContent.replace( + /webhook:.*/, + 'webhook: "https://test-webhook.example.com/hook"' + ); + } else { + modifiedSettings = + settingsContent + '\nwebhook: "https://test-webhook.example.com/hook"\n'; + } + await writeFile(`${tempDir}/settings.yaml`, modifiedSettings, "utf-8"); + + // Push the modification + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--include-settings"], + tempDir + ); + expect(pushResult.code).toEqual(0); + + // Verify via API + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/workspaces/get_settings` + ); + expect(apiResp.status).toEqual(200); + const settingsData = await apiResp.json(); + expect(settingsData.webhook).toEqual("https://test-webhook.example.com/hook"); + }); + }); +}); + +describe("resource type sync", () => { + test("Integration: Resource type pull/push round-trip", async () => { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const rtName = `test_sync_rt_${uniqueId}`; + + // Create a resource type via API + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/resources/type/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: rtName, + schema: { + type: "object", + properties: { + host: { type: "string", description: "Hostname" }, + port: { type: "integer", description: "Port number" }, + }, + }, + description: "Test resource type for sync", + }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + // Resource types are included by default (not skipped) + await writeWmillYaml(tempDir); + + // Pull + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); + expect(pullResult.code).toEqual(0); + + // Verify resource type file exists + const files = await listFilesRecursive(tempDir); + const rtFile = files.find((f) => f.includes(`${rtName}.resource-type.yaml`)); + expect(rtFile).toBeDefined(); + + // Read and modify the description + const rtContent = await readFile(`${tempDir}/${rtFile!}`, "utf-8"); + expect(rtContent).toContain("host"); + + const modifiedContent = rtContent.replace( + "Test resource type for sync", + "Updated resource type description" + ); + await writeFile(`${tempDir}/${rtFile!}`, modifiedContent, "utf-8"); + + // Push + const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); + expect(pushResult.code).toEqual(0); + + // Verify via API + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/resources/type/get/${rtName}` + ); + expect(apiResp.status).toEqual(200); + const rtData = await apiResp.json(); + expect(rtData.description).toEqual("Updated resource type description"); + }); + }); +}); + +describe("schedule sync", () => { + test("Integration: Schedule pull/push round-trip", async () => { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const scriptPath = `f/test/sched_sync_target_${uniqueId}`; + const schedulePath = `f/test/sched_sync_${uniqueId}`; + + // Create target script via API + await createRemoteScript(backend, scriptPath); + + // Create schedule via API + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/schedules/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: schedulePath, + schedule: "0 0 */6 * * *", + script_path: scriptPath, + is_flow: false, + args: {}, + enabled: false, + timezone: "UTC", + }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + await writeWmillYaml(tempDir, "includeSchedules: true"); + + // Pull with --include-schedules + const pullResult = await backend.runCLICommand( + ["sync", "pull", "--yes", "--include-schedules"], + tempDir + ); + expect(pullResult.code).toEqual(0); + + // Verify schedule file exists + const files = await listFilesRecursive(tempDir); + const scheduleFile = files.find( + (f) => f.includes(`sched_sync_${uniqueId}`) && f.endsWith(".schedule.yaml") + ); + expect(scheduleFile).toBeDefined(); + + // Read and verify content + const schedContent = await readFile(`${tempDir}/${scheduleFile!}`, "utf-8"); + expect(schedContent).toContain("0 0 */6 * * *"); + + // Modify the cron expression + const modifiedContent = schedContent.replace("0 0 */6 * * *", "0 0 */12 * * *"); + await writeFile(`${tempDir}/${scheduleFile!}`, modifiedContent, "utf-8"); + + // Push + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--include-schedules"], + tempDir + ); + expect(pushResult.code).toEqual(0); + + // Verify via API + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/schedules/get/${schedulePath}` + ); + expect(apiResp.status).toEqual(200); + const schedData = await apiResp.json(); + expect(schedData.schedule).toEqual("0 0 */12 * * *"); + }); + }); + + test("Integration: Schedule push-only creates from local file", async () => { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const scriptPath = `f/test/sched_pushonly_target_${uniqueId}`; + const schedulePath = `f/test/sched_pushonly_${uniqueId}`; + + // Create target script via API + await createRemoteScript(backend, scriptPath); + + await writeWmillYaml(tempDir, "includeSchedules: true"); + + // Create schedule YAML locally + await mkdir(`${tempDir}/f/test`, { recursive: true }); + await writeFile( + `${tempDir}/${schedulePath}.schedule.yaml`, + `path: "${schedulePath}" +schedule: "0 30 2 * * 1" +script_path: "${scriptPath}" +is_flow: false +args: {} +enabled: false +timezone: "UTC" +`, + "utf-8" + ); + + // Push + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--include-schedules", "--includes", `f/test/sched_pushonly_${uniqueId}**`], + tempDir + ); + expect(pushResult.code).toEqual(0); + + // Verify schedule was created via API + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/schedules/get/${schedulePath}` + ); + expect(apiResp.status).toEqual(200); + const schedData = await apiResp.json(); + expect(schedData.schedule).toEqual("0 30 2 * * 1"); + expect(schedData.script_path).toEqual(scriptPath); + }); + }); +}); + +describe("http trigger sync", () => { + test.skipIf(shouldSkipOnCI())("Integration: HTTP trigger pull/push is idempotent", async () => { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const scriptPath = `f/test/http_trig_target_${uniqueId}`; + + // Create target script via API + await createRemoteScript(backend, scriptPath); + + // Create HTTP trigger via API + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/http_triggers/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: `f/test/http_trig_${uniqueId}`, + script_path: scriptPath, + route_path: `/test/hook_${uniqueId}`, + is_flow: false, + http_method: "post", + is_async: false, + requires_auth: false, + }), + } + ); + // If the feature is not enabled, the create will fail - skip gracefully + if (createResp.status >= 400) { + console.log("HTTP trigger creation failed (feature may not be enabled), skipping"); + return; + } + await createResp.text(); + + await writeWmillYaml(tempDir, "includeTriggers: true"); + + // Pull with --include-triggers + const pullResult = await backend.runCLICommand( + ["sync", "pull", "--yes", "--include-triggers"], + tempDir + ); + expect(pullResult.code).toEqual(0); + + // Verify http_trigger file exists + const files = await listFilesRecursive(tempDir); + const triggerFile = files.find( + (f) => f.includes(`http_trig_${uniqueId}`) && f.endsWith(".http_trigger.yaml") + ); + expect(triggerFile).toBeDefined(); + + // Push back (verify idempotent) + const pushResult = await backend.runCLICommand( + ["sync", "push", "--dry-run", "--include-triggers"], + tempDir + ); + expect(pushResult.code).toEqual(0); + + const output = (pushResult.stdout + pushResult.stderr).toLowerCase(); + expect( + output.includes("0 change") || output.includes("no change") || output.includes("nothing") + ).toBeTruthy(); + }); + }); }); diff --git a/cli/test/test_backend.ts b/cli/test/test_backend.ts index b46166a21d..a3091d3a0e 100644 --- a/cli/test/test_backend.ts +++ b/cli/test/test_backend.ts @@ -13,7 +13,7 @@ * Usage: * import { withTestBackend, cleanupTestBackend } from "./test_backend.ts"; * - * Deno.test("my test", async () => { + * test("my test", async () => { * await withTestBackend(async (backend, tempDir) => { * const result = await backend.runCLICommand(["sync", "pull"], tempDir); * // ... @@ -23,6 +23,9 @@ import { CargoBackend, CargoBackendConfig } from "./cargo_backend.ts"; import { ContainerizedBackend, ContainerConfig } from "./containerized_backend.ts"; +import { mkdtemp, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; /** * Common interface for test backends @@ -37,7 +40,7 @@ export interface TestBackend { stop(): Promise; reset(): Promise; - createCLICommand(args: string[], workingDir: string, workspaceName?: string): Deno.Command; + createCLICommand(args: string[], workingDir: string, workspaceName?: string): any; runCLICommand(args: string[], workingDir: string, workspaceName?: string): Promise<{ stdout: string; stderr: string; @@ -94,7 +97,7 @@ class CargoBackendAdapter implements TestBackend { await this.backend.reset(); } - createCLICommand(args: string[], workingDir: string, workspaceName?: string): Deno.Command { + createCLICommand(args: string[], workingDir: string, workspaceName?: string): any { return this.backend.createCLICommand(args, workingDir, workspaceName); } @@ -366,7 +369,7 @@ class ContainerizedBackendAdapter implements TestBackend { await this.backend.reset(); } - createCLICommand(args: string[], workingDir: string, workspaceName?: string): Deno.Command { + createCLICommand(args: string[], workingDir: string, workspaceName?: string): any { return this.backend.createCLICommand(args, workingDir, workspaceName); } @@ -414,7 +417,7 @@ let globalBackend: TestBackend | null = null; * Get the backend type from environment */ function getBackendType(): "cargo" | "docker" { - const envType = Deno.env.get("TEST_BACKEND")?.toLowerCase(); + const envType = process.env["TEST_BACKEND"]?.toLowerCase(); if (envType === "docker") { return "docker"; } @@ -433,7 +436,7 @@ export function createTestBackend(type?: "cargo" | "docker"): TestBackend { } else { console.log("🦀 Using Cargo-based test backend"); return new CargoBackendAdapter({ - verbose: Deno.env.get("VERBOSE") === "1", + verbose: process.env["VERBOSE"] === "1", }); } } @@ -444,6 +447,7 @@ export function createTestBackend(type?: "cargo" | "docker"): TestBackend { export async function getTestBackend(): Promise { if (!globalBackend) { globalBackend = createTestBackend(); + registerCleanup(); await globalBackend.start(); } return globalBackend; @@ -456,7 +460,7 @@ export async function withTestBackend( testFn: (backend: TestBackend, tempDir: string) => Promise ): Promise { const backend = await getTestBackend(); - const tempDir = await Deno.makeTempDir({ prefix: "windmill_cli_test_" }); + const tempDir = await mkdtemp(join(tmpdir(), "windmill_cli_test_")); try { await backend.reset(); @@ -465,7 +469,7 @@ export async function withTestBackend( } return await testFn(backend, tempDir); } finally { - await Deno.remove(tempDir, { recursive: true }); + await rm(tempDir, { recursive: true }); } } @@ -479,6 +483,30 @@ export async function cleanupTestBackend(): Promise { } } +// Auto-cleanup on process exit +let cleanupRegistered = false; +function registerCleanup() { + if (cleanupRegistered) return; + cleanupRegistered = true; + process.on("exit", () => { + if (globalBackend) { + // Synchronous kill — can't await in exit handler + try { + (globalBackend as any).backend?.process?.kill(); + } catch { + // Best effort + } + } + }); + // Handle graceful shutdown + for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.on(signal, async () => { + await cleanupTestBackend(); + process.exit(0); + }); + } +} + // Re-export for convenience export type { CargoBackendConfig } from "./cargo_backend.ts"; export type { ContainerConfig } from "./containerized_backend.ts"; diff --git a/cli/test/test_config_helpers.ts b/cli/test/test_config_helpers.ts index c4b3816663..6b9ade42c1 100644 --- a/cli/test/test_config_helpers.ts +++ b/cli/test/test_config_helpers.ts @@ -1,3 +1,6 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; import { getWorkspaceConfigFilePath } from "../windmill-utils-internal/src/config/config.ts"; /** @@ -5,14 +8,14 @@ import { getWorkspaceConfigFilePath } from "../windmill-utils-internal/src/confi */ export async function withTestConfig(callback: (testConfigDir: string) => Promise): Promise { // Create a unique temporary directory for this test - const testDir = await Deno.makeTempDir({ prefix: "wmill_test_config_" }); - + const testDir = await mkdtemp(join(tmpdir(), "wmill_test_config_")); + try { return await callback(testDir); } finally { // Clean up the temporary directory try { - await Deno.remove(testDir, { recursive: true }); + await rm(testDir, { recursive: true }); } catch (error) { console.warn(`Failed to clean up test config directory ${testDir}:`, error); } @@ -24,7 +27,7 @@ export async function withTestConfig(callback: (testConfigDir: string) => Pro */ export async function clearTestRemotes(testConfigDir: string): Promise { const remoteFile = await getWorkspaceConfigFilePath(testConfigDir); - await Deno.writeTextFile(remoteFile, ""); + await writeFile(remoteFile, "", "utf-8"); } /** @@ -36,4 +39,4 @@ export function parseJsonFromCLIOutput(stdout: string): any { throw new Error(`No JSON found in CLI output: ${stdout}`); } return JSON.parse(jsonMatch[0]); -} \ No newline at end of file +} diff --git a/cli/test/utils_unit.test.ts b/cli/test/utils_unit.test.ts new file mode 100644 index 0000000000..f72ba23173 --- /dev/null +++ b/cli/test/utils_unit.test.ts @@ -0,0 +1,567 @@ +/** + * Unit tests for pure utility functions. + * These tests require no backend — they test standalone logic. + */ + +import { expect, test, describe } from "bun:test"; +import { deepEqual, isFileResource, toCamel, capitalize } from "../src/utils/utils.ts"; +import { + getTypeStrFromPath, + removeType, + isSuperset, + extractNativeTriggerInfo, + removePathPrefix, +} from "../src/types.ts"; +import { validatePath } from "../src/core/context.ts"; +import { inferContentTypeFromFilePath } from "../src/utils/script_common.ts"; +import { + filePathExtensionFromContentType, + removeExtensionToPath, +} from "../src/commands/script/script.ts"; + +// ============================================================================= +// deepEqual +// ============================================================================= + +describe("deepEqual", () => { + test("primitives", () => { + expect(deepEqual(1, 1)).toBe(true); + expect(deepEqual(1, 2)).toBe(false); + expect(deepEqual("a", "a")).toBe(true); + expect(deepEqual("a", "b")).toBe(false); + expect(deepEqual(true, true)).toBe(true); + expect(deepEqual(true, false)).toBe(false); + expect(deepEqual(null, null)).toBe(true); + expect(deepEqual(undefined, undefined)).toBe(true); + expect(deepEqual(null, undefined)).toBe(false); + }); + + test("NaN equality", () => { + expect(deepEqual(NaN, NaN)).toBe(true); + expect(deepEqual(NaN, 1)).toBe(false); + }); + + test("arrays", () => { + expect(deepEqual([1, 2, 3], [1, 2, 3])).toBe(true); + expect(deepEqual([1, 2, 3], [1, 2, 4])).toBe(false); + expect(deepEqual([1, 2], [1, 2, 3])).toBe(false); + expect(deepEqual([], [])).toBe(true); + }); + + test("nested arrays", () => { + expect(deepEqual([[1, 2], [3]], [[1, 2], [3]])).toBe(true); + expect(deepEqual([[1, 2], [3]], [[1, 2], [4]])).toBe(false); + }); + + test("objects", () => { + expect(deepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true); + expect(deepEqual({ a: 1, b: 2 }, { a: 1, b: 3 })).toBe(false); + expect(deepEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false); + expect(deepEqual({}, {})).toBe(true); + }); + + test("nested objects", () => { + expect(deepEqual({ a: { b: 1 } }, { a: { b: 1 } })).toBe(true); + expect(deepEqual({ a: { b: 1 } }, { a: { b: 2 } })).toBe(false); + }); + + test("mixed nested structures", () => { + const a = { arr: [1, { x: "hello" }], n: null }; + const b = { arr: [1, { x: "hello" }], n: null }; + expect(deepEqual(a, b)).toBe(true); + + const c = { arr: [1, { x: "world" }], n: null }; + expect(deepEqual(a, c)).toBe(false); + }); + + test("Maps", () => { + const m1 = new Map([["a", 1], ["b", 2]]); + const m2 = new Map([["a", 1], ["b", 2]]); + const m3 = new Map([["a", 1], ["b", 3]]); + expect(deepEqual(m1, m2)).toBe(true); + expect(deepEqual(m1, m3)).toBe(false); + }); + + test("Sets", () => { + const s1 = new Set([1, 2, 3]); + const s2 = new Set([1, 2, 3]); + const s3 = new Set([1, 2, 4]); + expect(deepEqual(s1, s2)).toBe(true); + expect(deepEqual(s1, s3)).toBe(false); + }); + + test("RegExp", () => { + expect(deepEqual(/abc/g, /abc/g)).toBe(true); + expect(deepEqual(/abc/g, /abc/i)).toBe(false); + expect(deepEqual(/abc/, /def/)).toBe(false); + }); +}); + +// ============================================================================= +// toCamel & capitalize +// ============================================================================= + +describe("toCamel", () => { + test("converts snake_case to camelCase", () => { + expect(toCamel("hello_world")).toBe("helloWorld"); + expect(toCamel("my_variable_name")).toBe("myVariableName"); + }); + + test("converts kebab-case to camelCase", () => { + expect(toCamel("hello-world")).toBe("helloWorld"); + }); + + test("handles no separators", () => { + expect(toCamel("hello")).toBe("hello"); + }); +}); + +describe("capitalize", () => { + test("capitalizes first character", () => { + expect(capitalize("hello")).toBe("Hello"); + expect(capitalize("world")).toBe("World"); + }); + + test("handles single character", () => { + expect(capitalize("a")).toBe("A"); + }); + + test("handles already capitalized", () => { + expect(capitalize("Hello")).toBe("Hello"); + }); + + test("handles empty string", () => { + expect(capitalize("")).toBe(""); + }); +}); + +// ============================================================================= +// isFileResource +// ============================================================================= + +describe("isFileResource", () => { + test("detects resource file paths", () => { + expect(isFileResource("f/test/my_file.resource.file.txt")).toBe(true); + expect(isFileResource("u/admin/config.resource.file.json")).toBe(true); + }); + + test("rejects non-resource-file paths", () => { + expect(isFileResource("f/test/my_resource.resource.yaml")).toBe(false); + expect(isFileResource("f/test/my_script.ts")).toBe(false); + expect(isFileResource("f/test/my_flow.flow/flow.yaml")).toBe(false); + }); + + test("detects branch-specific resource file paths", () => { + expect(isFileResource("f/test/config.main.resource.file.json")).toBe(true); + }); +}); + +// ============================================================================= +// removeType +// ============================================================================= + +describe("removeType", () => { + test("removes .variable.yaml suffix", () => { + expect(removeType("f/test/my_var.variable.yaml", "variable")).toBe("f/test/my_var"); + }); + + test("removes .resource.yaml suffix", () => { + expect(removeType("f/test/my_res.resource.yaml", "resource")).toBe("f/test/my_res"); + }); + + test("removes .schedule.yaml suffix", () => { + expect(removeType("u/admin/cron.schedule.yaml", "schedule")).toBe("u/admin/cron"); + }); + + test("removes .json suffix too", () => { + expect(removeType("f/test/my_var.variable.json", "variable")).toBe("f/test/my_var"); + }); + + test("throws for wrong type suffix", () => { + expect(() => removeType("f/test/my_var.variable.yaml", "resource")).toThrow(); + }); + + test("throws for no type suffix", () => { + expect(() => removeType("f/test/my_script.ts", "variable")).toThrow(); + }); +}); + +// ============================================================================= +// removePathPrefix +// ============================================================================= + +describe("removePathPrefix", () => { + test("removes prefix from path", () => { + expect(removePathPrefix("f/test/my_script.ts", "f/test")).toBe("my_script.ts"); + }); + + test("handles exact match", () => { + expect(removePathPrefix("f/test", "f/test")).toBe(""); + }); + + test("throws when prefix doesn't match", () => { + expect(() => removePathPrefix("g/admin/script.ts", "f/test")).toThrow(); + }); +}); + +// ============================================================================= +// getTypeStrFromPath +// ============================================================================= + +describe("getTypeStrFromPath", () => { + test("detects script types by extension", () => { + expect(getTypeStrFromPath("f/test/my_script.ts")).toBe("script"); + expect(getTypeStrFromPath("f/test/my_script.py")).toBe("script"); + expect(getTypeStrFromPath("f/test/my_script.go")).toBe("script"); + expect(getTypeStrFromPath("f/test/my_script.sh")).toBe("script"); + expect(getTypeStrFromPath("f/test/my_script.sql")).toBe("script"); + expect(getTypeStrFromPath("f/test/my_script.php")).toBe("script"); + expect(getTypeStrFromPath("f/test/my_script.rs")).toBe("script"); + }); + + test("detects metadata types by name suffix", () => { + expect(getTypeStrFromPath("f/test/my_var.variable.yaml")).toBe("variable"); + expect(getTypeStrFromPath("f/test/my_res.resource.yaml")).toBe("resource"); + expect(getTypeStrFromPath("f/test/my_sched.schedule.yaml")).toBe("schedule"); + expect(getTypeStrFromPath("f/test/my_rt.resource-type.yaml")).toBe("resource-type"); + }); + + test("detects trigger types", () => { + expect(getTypeStrFromPath("f/test/my_trig.http_trigger.yaml")).toBe("http_trigger"); + expect(getTypeStrFromPath("f/test/my_trig.websocket_trigger.yaml")).toBe("websocket_trigger"); + expect(getTypeStrFromPath("f/test/my_trig.kafka_trigger.yaml")).toBe("kafka_trigger"); + }); + + test("detects folder metadata", () => { + expect(getTypeStrFromPath("f/test/folder.meta.yaml")).toBe("folder"); + }); + + test("detects user and group", () => { + expect(getTypeStrFromPath("admin.user.yaml")).toBe("user"); + expect(getTypeStrFromPath("devs.group.yaml")).toBe("group"); + }); + + test("throws for unknown type", () => { + expect(() => getTypeStrFromPath("f/test/unknown.xyz.yaml")).toThrow(); + }); +}); + +// ============================================================================= +// validatePath +// ============================================================================= + +describe("validatePath", () => { + test("accepts valid paths", () => { + expect(validatePath("f/test/my_script")).toBe(true); + expect(validatePath("u/admin/my_script")).toBe(true); + expect(validatePath("g/all/my_script")).toBe(true); + }); + + test("rejects invalid paths", () => { + expect(validatePath("invalid/path")).toBe(false); + expect(validatePath("test/my_script")).toBe(false); + }); +}); + +// ============================================================================= +// inferContentTypeFromFilePath +// ============================================================================= + +describe("inferContentTypeFromFilePath", () => { + test("detects Python", () => { + expect(inferContentTypeFromFilePath("script.py", undefined)).toBe("python3"); + }); + + test("detects Go", () => { + expect(inferContentTypeFromFilePath("script.go", undefined)).toBe("go"); + }); + + test("detects Bash", () => { + expect(inferContentTypeFromFilePath("script.sh", undefined)).toBe("bash"); + }); + + test("detects PHP", () => { + expect(inferContentTypeFromFilePath("script.php", undefined)).toBe("php"); + }); + + test("detects Rust", () => { + expect(inferContentTypeFromFilePath("script.rs", undefined)).toBe("rust"); + }); + + test("detects PowerShell", () => { + expect(inferContentTypeFromFilePath("script.ps1", undefined)).toBe("powershell"); + }); + + test("detects GraphQL", () => { + expect(inferContentTypeFromFilePath("query.gql", undefined)).toBe("graphql"); + }); + + test("defaults .ts to bun", () => { + expect(inferContentTypeFromFilePath("script.ts", undefined)).toBe("bun"); + }); + + test("uses defaultTs for .ts files", () => { + expect(inferContentTypeFromFilePath("script.ts", "deno")).toBe("deno"); + expect(inferContentTypeFromFilePath("script.ts", "bun")).toBe("bun"); + }); + + test("explicit bun.ts and deno.ts override defaultTs", () => { + expect(inferContentTypeFromFilePath("script.bun.ts", "deno")).toBe("bun"); + expect(inferContentTypeFromFilePath("script.deno.ts", "bun")).toBe("deno"); + }); + + test("detects nativets with fetch.ts", () => { + expect(inferContentTypeFromFilePath("script.fetch.ts", "bun")).toBe("nativets"); + }); + + test("detects SQL variants", () => { + expect(inferContentTypeFromFilePath("query.pg.sql", undefined)).toBe("postgresql"); + expect(inferContentTypeFromFilePath("query.my.sql", undefined)).toBe("mysql"); + expect(inferContentTypeFromFilePath("query.bq.sql", undefined)).toBe("bigquery"); + expect(inferContentTypeFromFilePath("query.ms.sql", undefined)).toBe("mssql"); + expect(inferContentTypeFromFilePath("query.sf.sql", undefined)).toBe("snowflake"); + expect(inferContentTypeFromFilePath("query.duckdb.sql", undefined)).toBe("duckdb"); + expect(inferContentTypeFromFilePath("query.odb.sql", undefined)).toBe("oracledb"); + }); +}); + +// ============================================================================= +// extractNativeTriggerInfo +// ============================================================================= + +describe("extractNativeTriggerInfo", () => { + test("extracts info from valid flow trigger path", () => { + const result = extractNativeTriggerInfo( + "u/admin/script.flow.12345.nextcloud_native_trigger.json" + ); + expect(result).not.toBeNull(); + expect(result!.scriptPath).toBe("u/admin/script"); + expect(result!.isFlow).toBe(true); + expect(result!.externalId).toBe("12345"); + expect(result!.serviceName).toBe("nextcloud"); + }); + + test("detects script (non-flow) triggers", () => { + const result = extractNativeTriggerInfo( + "f/test/handler.script.abc123.nextcloud_native_trigger.json" + ); + expect(result).not.toBeNull(); + expect(result!.isFlow).toBe(false); + expect(result!.scriptPath).toBe("f/test/handler"); + }); + + test("returns null for non-native trigger paths", () => { + expect(extractNativeTriggerInfo("f/test/my_var.variable.yaml")).toBeNull(); + expect(extractNativeTriggerInfo("f/test/trig.http_trigger.yaml")).toBeNull(); + }); +}); + +// ============================================================================= +// isSuperset +// ============================================================================= + +describe("isSuperset", () => { + test("returns true when subset matches superset", () => { + expect(isSuperset({ a: 1 }, { a: 1, b: 2 })).toBe(true); + }); + + test("returns true when objects are identical", () => { + expect(isSuperset({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true); + }); + + test("returns false when values differ", () => { + expect(isSuperset({ a: 1 }, { a: 2 })).toBe(false); + }); + + test("handles nested objects", () => { + expect(isSuperset({ a: { x: 1 } }, { a: { x: 1 }, b: 2 })).toBe(true); + expect(isSuperset({ a: { x: 1 } }, { a: { x: 2 } })).toBe(false); + }); + + test("empty subset is always a superset match", () => { + expect(isSuperset({}, { a: 1, b: 2 })).toBe(true); + }); +}); + +// ============================================================================= +// filePathExtensionFromContentType +// ============================================================================= + +describe("filePathExtensionFromContentType", () => { + test("returns .py for python3", () => { + expect(filePathExtensionFromContentType("python3", undefined)).toBe(".py"); + }); + + test("returns .fetch.ts for nativets", () => { + expect(filePathExtensionFromContentType("nativets", undefined)).toBe(".fetch.ts"); + }); + + test("returns .ts for bun when defaultTs is bun or undefined", () => { + expect(filePathExtensionFromContentType("bun", "bun")).toBe(".ts"); + expect(filePathExtensionFromContentType("bun", undefined)).toBe(".ts"); + }); + + test("returns .bun.ts for bun when defaultTs is deno", () => { + expect(filePathExtensionFromContentType("bun", "deno")).toBe(".bun.ts"); + }); + + test("returns .ts for deno when defaultTs is deno", () => { + expect(filePathExtensionFromContentType("deno", "deno")).toBe(".ts"); + }); + + test("returns .deno.ts for deno when defaultTs is bun or undefined", () => { + expect(filePathExtensionFromContentType("deno", "bun")).toBe(".deno.ts"); + expect(filePathExtensionFromContentType("deno", undefined)).toBe(".deno.ts"); + }); + + test("returns .go for go", () => { + expect(filePathExtensionFromContentType("go", undefined)).toBe(".go"); + }); + + test("returns .sh for bash", () => { + expect(filePathExtensionFromContentType("bash", undefined)).toBe(".sh"); + }); + + test("returns .ps1 for powershell", () => { + expect(filePathExtensionFromContentType("powershell", undefined)).toBe(".ps1"); + }); + + test("returns .gql for graphql", () => { + expect(filePathExtensionFromContentType("graphql", undefined)).toBe(".gql"); + }); + + test("returns .php for php", () => { + expect(filePathExtensionFromContentType("php", undefined)).toBe(".php"); + }); + + test("returns .rs for rust", () => { + expect(filePathExtensionFromContentType("rust", undefined)).toBe(".rs"); + }); + + test("returns .cs for csharp", () => { + expect(filePathExtensionFromContentType("csharp", undefined)).toBe(".cs"); + }); + + test("returns .nu for nu", () => { + expect(filePathExtensionFromContentType("nu", undefined)).toBe(".nu"); + }); + + test("returns .java for java", () => { + expect(filePathExtensionFromContentType("java", undefined)).toBe(".java"); + }); + + test("returns .rb for ruby", () => { + expect(filePathExtensionFromContentType("ruby", undefined)).toBe(".rb"); + }); + + test("returns .playbook.yml for ansible", () => { + expect(filePathExtensionFromContentType("ansible", undefined)).toBe(".playbook.yml"); + }); + + test("returns correct SQL extensions", () => { + expect(filePathExtensionFromContentType("postgresql", undefined)).toBe(".pg.sql"); + expect(filePathExtensionFromContentType("mysql", undefined)).toBe(".my.sql"); + expect(filePathExtensionFromContentType("bigquery", undefined)).toBe(".bq.sql"); + expect(filePathExtensionFromContentType("duckdb", undefined)).toBe(".duckdb.sql"); + expect(filePathExtensionFromContentType("oracledb", undefined)).toBe(".odb.sql"); + expect(filePathExtensionFromContentType("snowflake", undefined)).toBe(".sf.sql"); + expect(filePathExtensionFromContentType("mssql", undefined)).toBe(".ms.sql"); + }); + + test("throws for invalid language", () => { + expect(() => + filePathExtensionFromContentType("invalid" as any, undefined) + ).toThrow(); + }); +}); + +// ============================================================================= +// removeExtensionToPath +// ============================================================================= + +describe("removeExtensionToPath", () => { + test("removes .ts extension", () => { + expect(removeExtensionToPath("f/test/script.ts")).toBe("f/test/script"); + }); + + test("removes .py extension", () => { + expect(removeExtensionToPath("f/test/script.py")).toBe("f/test/script"); + }); + + test("removes .go extension", () => { + expect(removeExtensionToPath("f/test/script.go")).toBe("f/test/script"); + }); + + test("removes .sh extension", () => { + expect(removeExtensionToPath("f/test/script.sh")).toBe("f/test/script"); + }); + + test("removes .pg.sql extension", () => { + expect(removeExtensionToPath("f/test/query.pg.sql")).toBe("f/test/query"); + }); + + test("removes .my.sql extension", () => { + expect(removeExtensionToPath("f/test/query.my.sql")).toBe("f/test/query"); + }); + + test("removes .duckdb.sql extension", () => { + expect(removeExtensionToPath("f/test/query.duckdb.sql")).toBe("f/test/query"); + }); + + test("removes .fetch.ts extension", () => { + expect(removeExtensionToPath("f/test/script.fetch.ts")).toBe("f/test/script"); + }); + + test("removes .bun.ts extension", () => { + expect(removeExtensionToPath("f/test/script.bun.ts")).toBe("f/test/script"); + }); + + test("removes .deno.ts extension", () => { + expect(removeExtensionToPath("f/test/script.deno.ts")).toBe("f/test/script"); + }); + + test("removes .gql extension", () => { + expect(removeExtensionToPath("f/test/query.gql")).toBe("f/test/query"); + }); + + test("removes .ps1 extension", () => { + expect(removeExtensionToPath("f/test/script.ps1")).toBe("f/test/script"); + }); + + test("removes .php extension", () => { + expect(removeExtensionToPath("f/test/script.php")).toBe("f/test/script"); + }); + + test("removes .rs extension", () => { + expect(removeExtensionToPath("f/test/script.rs")).toBe("f/test/script"); + }); + + test("removes .cs extension", () => { + expect(removeExtensionToPath("f/test/script.cs")).toBe("f/test/script"); + }); + + test("removes .nu extension", () => { + expect(removeExtensionToPath("f/test/script.nu")).toBe("f/test/script"); + }); + + test("removes .playbook.yml extension", () => { + expect(removeExtensionToPath("f/test/play.playbook.yml")).toBe("f/test/play"); + }); + + test("removes .java extension", () => { + expect(removeExtensionToPath("f/test/Script.java")).toBe("f/test/Script"); + }); + + test("removes .rb extension", () => { + expect(removeExtensionToPath("f/test/script.rb")).toBe("f/test/script"); + }); + + test("throws for unknown extension", () => { + expect(() => removeExtensionToPath("f/test/file.xyz")).toThrow(); + }); + + test("prioritizes longer extensions (fetch.ts over .ts)", () => { + // fetch.ts should be recognized as nativets, not as bun .ts + expect(removeExtensionToPath("f/test/api.fetch.ts")).toBe("f/test/api"); + }); +}); diff --git a/cli/test/variable_resource_push.test.ts b/cli/test/variable_resource_push.test.ts new file mode 100644 index 0000000000..c348a31e66 --- /dev/null +++ b/cli/test/variable_resource_push.test.ts @@ -0,0 +1,340 @@ +/** + * Integration tests for variable and resource CLI commands. + * Tests list and push operations via CLI and direct API. + */ + +import { expect, test, describe } from "bun:test"; +import { writeFile, mkdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { withTestBackend } from "./test_backend.ts"; +import { addWorkspace } from "../workspace.ts"; + +async function setupWorkspaceProfile(backend: any): Promise { + await addWorkspace( + { + remote: backend.baseUrl, + workspaceId: backend.workspace, + name: "localhost_test", + token: backend.token, + }, + { force: true, configDir: backend.testConfigDir } + ); +} + +// ============================================================================= +// Variable Tests +// ============================================================================= + +describe("variable", () => { + test("list returns seeded variables", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand(["variable"], tempDir); + + expect(result.code).toEqual(0); + // seedTestData creates f/test/my_variable + expect(result.stdout).toContain("f/test/my_variable"); + }); + }); + + test("push creates a new variable via sync push", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + + // Create wmill.yaml + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + + // Create variable file + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + const varPath = `f/test/test_var_${uniqueId}.variable.yaml`; + await writeFile( + join(tempDir, varPath), + `value: "hello_from_test_${uniqueId}"\nis_secret: false\ndescription: "Test variable created by integration test"\n`, + "utf-8" + ); + + // Push with sync push targeting just our variable + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--includes", `f/test/test_var_${uniqueId}**`], + tempDir + ); + + expect(pushResult.code).toEqual(0); + + // Verify via API that the variable was created + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/variables/get/f/test/test_var_${uniqueId}` + ); + expect(apiResp.status).toEqual(200); + const varData = await apiResp.json(); + expect(varData.path).toBe(`f/test/test_var_${uniqueId}`); + expect(varData.is_secret).toBe(false); + }); + }); + + test("push updates an existing variable", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + + // Create variable via API first + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/variables/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: `f/test/update_var_${uniqueId}`, + value: "original_value", + is_secret: false, + description: "Original description", + }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + // Create wmill.yaml and updated variable file + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + await writeFile( + join(tempDir, `f/test/update_var_${uniqueId}.variable.yaml`), + `value: "updated_value"\nis_secret: false\ndescription: "Updated description"\n`, + "utf-8" + ); + + // Push the update + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--includes", `f/test/update_var_${uniqueId}**`], + tempDir + ); + + expect(pushResult.code).toEqual(0); + + // Verify the update via API + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/variables/get/f/test/update_var_${uniqueId}` + ); + expect(apiResp.status).toEqual(200); + const varData = await apiResp.json(); + expect(varData.description).toBe("Updated description"); + }); + }); + + test("pull retrieves variables into local files", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + + // Create a variable via API + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/variables/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: `f/test/pull_var_${uniqueId}`, + value: "pull_test_value", + is_secret: false, + description: "Variable for pull test", + }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + // Create wmill.yaml + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "f/test/pull_var_${uniqueId}**"\nexcludes: []\n`, + "utf-8" + ); + + // Pull + const pullResult = await backend.runCLICommand( + ["sync", "pull", "--yes"], + tempDir + ); + expect(pullResult.code).toEqual(0); + + // Check the file was created + const content = await readFile( + join(tempDir, `f/test/pull_var_${uniqueId}.variable.yaml`), "utf-8" + ); + expect(content).toContain("pull_test_value"); + expect(content).toContain("is_secret: false"); + }); + }); +}); + +// ============================================================================= +// Resource Tests +// ============================================================================= + +describe("resource", () => { + test("list returns seeded resources", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand(["resource"], tempDir); + + expect(result.code).toEqual(0); + // seedTestData creates f/test/my_resource + expect(result.stdout).toContain("f/test/my_resource"); + }); + }); + + test("push creates a new resource via sync push", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + + // Create wmill.yaml + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + + // Create resource file + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + const resPath = `f/test/test_res_${uniqueId}.resource.yaml`; + await writeFile( + join(tempDir, resPath), + `resource_type: "any"\nvalue:\n host: "localhost"\n port: 3000\ndescription: "Test resource"\n`, + "utf-8" + ); + + // Push + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--includes", `f/test/test_res_${uniqueId}**`], + tempDir + ); + + expect(pushResult.code).toEqual(0); + + // Verify via API + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/resources/get/f/test/test_res_${uniqueId}` + ); + expect(apiResp.status).toEqual(200); + const resData = await apiResp.json(); + expect(resData.path).toBe(`f/test/test_res_${uniqueId}`); + expect(resData.resource_type).toBe("any"); + expect(resData.value.host).toBe("localhost"); + }); + }); + + test("push updates an existing resource", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + + // Create resource via API + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/resources/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: `f/test/update_res_${uniqueId}`, + resource_type: "any", + value: { host: "old_host" }, + description: "Original", + }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + // Create wmill.yaml and updated resource file + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + await writeFile( + join(tempDir, `f/test/update_res_${uniqueId}.resource.yaml`), + `resource_type: "any"\nvalue:\n host: "new_host"\n port: 9999\ndescription: "Updated"\n`, + "utf-8" + ); + + // Push the update + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--includes", `f/test/update_res_${uniqueId}**`], + tempDir + ); + + expect(pushResult.code).toEqual(0); + + // Verify update + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/resources/get/f/test/update_res_${uniqueId}` + ); + expect(apiResp.status).toEqual(200); + const resData = await apiResp.json(); + expect(resData.value.host).toBe("new_host"); + expect(resData.value.port).toBe(9999); + }); + }); + + test("pull retrieves resources into local files", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + + // Create resource via API + const createResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/resources/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: `f/test/pull_res_${uniqueId}`, + resource_type: "any", + value: { key: "pull_test" }, + }), + } + ); + expect(createResp.status).toBeLessThan(300); + await createResp.text(); + + // Create wmill.yaml + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "f/test/pull_res_${uniqueId}**"\nexcludes: []\nskipVariables: true\n`, + "utf-8" + ); + + // Pull + const pullResult = await backend.runCLICommand( + ["sync", "pull", "--yes"], + tempDir + ); + expect(pullResult.code).toEqual(0); + + // Check the resource file was created + const content = await readFile( + join(tempDir, `f/test/pull_res_${uniqueId}.resource.yaml`), "utf-8" + ); + expect(content).toContain("pull_test"); + }); + }); +}); diff --git a/cli/test/wmill_lock.test.ts b/cli/test/wmill_lock.test.ts index f3a635a36e..7760166fd4 100644 --- a/cli/test/wmill_lock.test.ts +++ b/cli/test/wmill_lock.test.ts @@ -6,9 +6,10 @@ * looked up on both Windows and Linux systems. */ -import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts"; -import * as path from "https://deno.land/std@0.224.0/path/mod.ts"; -import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts"; +import { expect, test } from "bun:test"; +import * as path from "@std/path"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; import { normalizeLockPath, readLockfile, @@ -17,30 +18,31 @@ import { clearGlobalLock, } from "../src/utils/metadata.ts"; import { generateHash } from "../src/utils/utils.ts"; -import { yamlStringify, yamlParseFile } from "../deps.ts"; +import { stringify as yamlStringify } from "@std/yaml"; +import { yamlParseFile } from "../src/utils/yaml.ts"; // ============================================================================= // UNIT TESTS - Path Normalization // ============================================================================= -Deno.test("normalizeLockPath: converts Windows backslashes to forward slashes", () => { - assertEquals(normalizeLockPath("f\\test\\script"), "f/test/script"); - assertEquals(normalizeLockPath("f\\deeply\\nested\\path\\script"), "f/deeply/nested/path/script"); +test("normalizeLockPath: converts Windows backslashes to forward slashes", () => { + expect(normalizeLockPath("f\\test\\script")).toEqual("f/test/script"); + expect(normalizeLockPath("f\\deeply\\nested\\path\\script")).toEqual("f/deeply/nested/path/script"); }); -Deno.test("normalizeLockPath: preserves already-normalized paths", () => { - assertEquals(normalizeLockPath("f/test/script"), "f/test/script"); - assertEquals(normalizeLockPath("f/deeply/nested/path/script"), "f/deeply/nested/path/script"); +test("normalizeLockPath: preserves already-normalized paths", () => { + expect(normalizeLockPath("f/test/script")).toEqual("f/test/script"); + expect(normalizeLockPath("f/deeply/nested/path/script")).toEqual("f/deeply/nested/path/script"); }); -Deno.test("normalizeLockPath: handles paths without separators", () => { - assertEquals(normalizeLockPath("script"), "script"); - assertEquals(normalizeLockPath(""), ""); +test("normalizeLockPath: handles paths without separators", () => { + expect(normalizeLockPath("script")).toEqual("script"); + expect(normalizeLockPath("")).toEqual(""); }); -Deno.test("normalizeLockPath: handles mixed separators", () => { - assertEquals(normalizeLockPath("f/test\\nested/script"), "f/test/nested/script"); - assertEquals(normalizeLockPath("f\\test/nested\\script"), "f/test/nested/script"); +test("normalizeLockPath: handles mixed separators", () => { + expect(normalizeLockPath("f/test\\nested/script")).toEqual("f/test/nested/script"); + expect(normalizeLockPath("f\\test/nested\\script")).toEqual("f/test/nested/script"); }); // ============================================================================= @@ -48,18 +50,18 @@ Deno.test("normalizeLockPath: handles mixed separators", () => { // ============================================================================= async function withTempDir(fn: (tempDir: string) => Promise): Promise { - const tempDir = await Deno.makeTempDir({ prefix: "wmill_lock_test_" }); - const originalCwd = Deno.cwd(); + const tempDir = await mkdtemp(path.join(os.tmpdir(), "wmill_lock_test_")); + const originalCwd = process.cwd(); try { - Deno.chdir(tempDir); + process.chdir(tempDir); await fn(tempDir); } finally { - Deno.chdir(originalCwd); - await Deno.remove(tempDir, { recursive: true }); + process.chdir(originalCwd); + await rm(tempDir, { recursive: true }); } } -Deno.test("wmill-lock: stores paths with Linux separators even when given Windows paths", async () => { +test("wmill-lock: stores paths with Linux separators even when given Windows paths", async () => { await withTempDir(async (tempDir) => { // Simulate a Windows-style path const windowsPath = "f\\flows\\my-flow.flow"; @@ -71,12 +73,12 @@ Deno.test("wmill-lock: stores paths with Linux separators even when given Window const lockfile = await yamlParseFile("wmill-lock.yaml") as { version: string; locks: Record }; // Path should be stored with forward slashes - assertEquals(lockfile.locks["f/flows/my-flow.flow"], hash); - assertEquals(lockfile.locks["f\\flows\\my-flow.flow"], undefined); + expect(lockfile.locks["f/flows/my-flow.flow"]).toEqual(hash); + expect(lockfile.locks["f\\flows\\my-flow.flow"]).toEqual(undefined); }); }); -Deno.test("wmill-lock: checkifMetadataUptodate finds paths regardless of separator style", async () => { +test("wmill-lock: checkifMetadataUptodate finds paths regardless of separator style", async () => { await withTempDir(async (tempDir) => { const linuxPath = "f/scripts/my-script"; const windowsPath = "f\\scripts\\my-script"; @@ -87,18 +89,18 @@ Deno.test("wmill-lock: checkifMetadataUptodate finds paths regardless of separat // Should find with Linux-style lookup const conf = await readLockfile(); - assertEquals(await checkifMetadataUptodate(linuxPath, hash, conf), true); + expect(await checkifMetadataUptodate(linuxPath, hash, conf)).toEqual(true); // Should also find with Windows-style lookup (simulating Windows usage) - assertEquals(await checkifMetadataUptodate(windowsPath, hash, conf), true); + expect(await checkifMetadataUptodate(windowsPath, hash, conf)).toEqual(true); // Should not find with wrong hash - assertEquals(await checkifMetadataUptodate(linuxPath, "wrong", conf), false); - assertEquals(await checkifMetadataUptodate(windowsPath, "wrong", conf), false); + expect(await checkifMetadataUptodate(linuxPath, "wrong", conf)).toEqual(false); + expect(await checkifMetadataUptodate(windowsPath, "wrong", conf)).toEqual(false); }); }); -Deno.test("wmill-lock: updateMetadataGlobalLock with subpath normalizes both path and subpath", async () => { +test("wmill-lock: updateMetadataGlobalLock with subpath normalizes both path and subpath", async () => { await withTempDir(async (tempDir) => { const windowsPath = "f\\flows\\my-flow.flow"; const windowsSubpath = "inline\\script.ts"; @@ -110,11 +112,11 @@ Deno.test("wmill-lock: updateMetadataGlobalLock with subpath normalizes both pat const lockfile = await yamlParseFile("wmill-lock.yaml") as { version: string; locks: Record }; // Both path and subpath should use forward slashes - assertEquals(lockfile.locks["f/flows/my-flow.flow+inline/script.ts"], hash); + expect(lockfile.locks["f/flows/my-flow.flow+inline/script.ts"]).toEqual(hash); }); }); -Deno.test("wmill-lock: checkifMetadataUptodate with subpath handles Windows separators", async () => { +test("wmill-lock: checkifMetadataUptodate with subpath handles Windows separators", async () => { await withTempDir(async (tempDir) => { const linuxPath = "f/apps/my-app.app"; const linuxSubpath = "scripts/button.ts"; @@ -128,18 +130,18 @@ Deno.test("wmill-lock: checkifMetadataUptodate with subpath handles Windows sepa const conf = await readLockfile(); // Should find with Linux-style lookup - assertEquals(await checkifMetadataUptodate(linuxPath, hash, conf, linuxSubpath), true); + expect(await checkifMetadataUptodate(linuxPath, hash, conf, linuxSubpath)).toEqual(true); // Should find with Windows-style lookup - assertEquals(await checkifMetadataUptodate(windowsPath, hash, conf, windowsSubpath), true); + expect(await checkifMetadataUptodate(windowsPath, hash, conf, windowsSubpath)).toEqual(true); // Should find with mixed-style lookup - assertEquals(await checkifMetadataUptodate(windowsPath, hash, conf, linuxSubpath), true); - assertEquals(await checkifMetadataUptodate(linuxPath, hash, conf, windowsSubpath), true); + expect(await checkifMetadataUptodate(windowsPath, hash, conf, linuxSubpath)).toEqual(true); + expect(await checkifMetadataUptodate(linuxPath, hash, conf, windowsSubpath)).toEqual(true); }); }); -Deno.test("wmill-lock: clearGlobalLock clears paths regardless of separator style", async () => { +test("wmill-lock: clearGlobalLock clears paths regardless of separator style", async () => { await withTempDir(async (tempDir) => { const basePath = "f/flows/my-flow.flow"; const subpath1 = "scripts/a.ts"; @@ -152,21 +154,21 @@ Deno.test("wmill-lock: clearGlobalLock clears paths regardless of separator styl // Verify they exist let conf = await readLockfile(); - assertEquals(await checkifMetadataUptodate(basePath, "hash1", conf, subpath1), true); - assertEquals(await checkifMetadataUptodate(basePath, "hash2", conf, subpath2), true); + expect(await checkifMetadataUptodate(basePath, "hash1", conf, subpath1)).toEqual(true); + expect(await checkifMetadataUptodate(basePath, "hash2", conf, subpath2)).toEqual(true); // Clear using Windows-style path await clearGlobalLock("f\\flows\\my-flow.flow"); // All entries should be cleared conf = await readLockfile(); - assertEquals(await checkifMetadataUptodate(basePath, "hash1", conf, subpath1), false); - assertEquals(await checkifMetadataUptodate(basePath, "hash2", conf, subpath2), false); - assertEquals(await checkifMetadataUptodate(basePath, "topHash", conf, "__flow_hash"), false); + expect(await checkifMetadataUptodate(basePath, "hash1", conf, subpath1)).toEqual(false); + expect(await checkifMetadataUptodate(basePath, "hash2", conf, subpath2)).toEqual(false); + expect(await checkifMetadataUptodate(basePath, "topHash", conf, "__flow_hash")).toEqual(false); }); }); -Deno.test("wmill-lock: lock file created on Linux can be used on Windows (simulated)", async () => { +test("wmill-lock: lock file created on Linux can be used on Windows (simulated)", async () => { await withTempDir(async (tempDir) => { // Simulate a lock file created on Linux const linuxLockContent = { @@ -178,21 +180,22 @@ Deno.test("wmill-lock: lock file created on Linux can be used on Windows (simula }, }; - await Deno.writeTextFile( + await writeFile( "wmill-lock.yaml", - yamlStringify(linuxLockContent as Record) + yamlStringify(linuxLockContent as Record), + "utf-8" ); const conf = await readLockfile(); // Simulate Windows lookups (using backslashes) - assertEquals(await checkifMetadataUptodate("f\\scripts\\utility", "hash1", conf), true); - assertEquals(await checkifMetadataUptodate("f\\flows\\main.flow", "hash2", conf, "scripts\\step1.ts"), true); - assertEquals(await checkifMetadataUptodate("f\\apps\\dashboard.app", "hash3", conf, "components\\chart.ts"), true); + expect(await checkifMetadataUptodate("f\\scripts\\utility", "hash1", conf)).toEqual(true); + expect(await checkifMetadataUptodate("f\\flows\\main.flow", "hash2", conf, "scripts\\step1.ts")).toEqual(true); + expect(await checkifMetadataUptodate("f\\apps\\dashboard.app", "hash3", conf, "components\\chart.ts")).toEqual(true); }); }); -Deno.test("wmill-lock: multiple updates with different separator styles result in single entry", async () => { +test("wmill-lock: multiple updates with different separator styles result in single entry", async () => { await withTempDir(async (tempDir) => { const linuxPath = "f/scripts/shared"; const windowsPath = "f\\scripts\\shared"; @@ -207,9 +210,9 @@ Deno.test("wmill-lock: multiple updates with different separator styles result i // Should only have one entry with the latest hash const lockKeys = Object.keys(lockfile.locks); - assertEquals(lockKeys.length, 1); - assertEquals(lockKeys[0], "f/scripts/shared"); - assertEquals(lockfile.locks["f/scripts/shared"], "hash2"); + expect(lockKeys.length).toEqual(1); + expect(lockKeys[0]).toEqual("f/scripts/shared"); + expect(lockfile.locks["f/scripts/shared"]).toEqual("hash2"); }); }); @@ -217,7 +220,7 @@ Deno.test("wmill-lock: multiple updates with different separator styles result i // HASH COMPUTATION TESTS - OS-Independent Hash Generation // ============================================================================= -Deno.test("hash computation: normalized paths produce same hash on Windows and Linux", async () => { +test("hash computation: normalized paths produce same hash on Windows and Linux", async () => { // Simulate how generateFlowHash/generateAppHash compute hashes // by using paths as keys in an object that gets stringified @@ -246,13 +249,13 @@ Deno.test("hash computation: normalized paths produce same hash on Windows and L const linuxTopHash = await generateHash(JSON.stringify(linuxHashes)); // Both should produce the same top hash - assertEquals(windowsTopHash, linuxTopHash); + expect(windowsTopHash).toEqual(linuxTopHash); // And the individual hashes should have the same keys - assertEquals(Object.keys(windowsHashes).sort(), Object.keys(linuxHashes).sort()); + expect(Object.keys(windowsHashes).sort()).toEqual(Object.keys(linuxHashes).sort()); }); -Deno.test("hash computation: without normalization, Windows and Linux would produce different hashes", async () => { +test("hash computation: without normalization, Windows and Linux would produce different hashes", async () => { // This test demonstrates the problem that normalization fixes const fileContents = { "script1.ts": "export function main() { return 1; }", @@ -282,13 +285,13 @@ Deno.test("hash computation: without normalization, Windows and Linux would prod const linuxKeys = Object.keys(linuxHashesNoNormalize).sort(); // Keys should be different without normalization - assertEquals(windowsKeys.includes("nested\\script2.ts"), true); - assertEquals(linuxKeys.includes("nested/script2.ts"), true); - assertEquals(windowsKeys.includes("nested/script2.ts"), false); - assertEquals(linuxKeys.includes("nested\\script2.ts"), false); + expect(windowsKeys.includes("nested\\script2.ts")).toEqual(true); + expect(linuxKeys.includes("nested/script2.ts")).toEqual(true); + expect(windowsKeys.includes("nested/script2.ts")).toEqual(false); + expect(linuxKeys.includes("nested\\script2.ts")).toEqual(false); }); -Deno.test("hash computation: deeply nested paths are normalized correctly", async () => { +test("hash computation: deeply nested paths are normalized correctly", async () => { const deepWindowsPath = "f\\flows\\my-flow.flow\\inline\\scripts\\deeply\\nested\\handler.ts"; const deepLinuxPath = "f/flows/my-flow.flow/inline/scripts/deeply/nested/handler.ts"; @@ -304,12 +307,12 @@ Deno.test("hash computation: deeply nested paths are normalized correctly", asyn linuxHashes[normalizeLockPath(deepLinuxPath)] = await generateHash(content); const linuxTopHash = await generateHash(JSON.stringify(linuxHashes)); - assertEquals(windowsTopHash, linuxTopHash); - assertEquals(Object.keys(windowsHashes)[0], Object.keys(linuxHashes)[0]); - assertEquals(Object.keys(windowsHashes)[0], deepLinuxPath); + expect(windowsTopHash).toEqual(linuxTopHash); + expect(Object.keys(windowsHashes)[0]).toEqual(Object.keys(linuxHashes)[0]); + expect(Object.keys(windowsHashes)[0]).toEqual(deepLinuxPath); }); -Deno.test("hash computation: changedScripts comparison works with inline module paths", () => { +test("hash computation: changedScripts comparison works with inline module paths", () => { // This test simulates the comparison done in replaceInlineScripts // where changedScripts (from hashes keys) is compared with paths from flow module content @@ -329,10 +332,8 @@ Deno.test("hash computation: changedScripts comparison works with inline module // All inline module paths should be found in changedScripts for (const inlinePath of inlineModulePaths) { - assertEquals( - changedScripts.includes(inlinePath), - true, - `Expected changedScripts to include "${inlinePath}"` - ); + expect( + changedScripts.includes(inlinePath) + ).toEqual(true); } }); diff --git a/cli/test/workspace_conflicts.test.ts b/cli/test/workspace_conflicts.test.ts index 1338b7458e..3083d53218 100644 --- a/cli/test/workspace_conflicts.test.ts +++ b/cli/test/workspace_conflicts.test.ts @@ -1,22 +1,22 @@ -import { assertEquals, assertRejects } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; import { addWorkspace, allWorkspaces } from "../workspace.ts"; import { withTestConfig, clearTestRemotes } from "./test_config_helpers.ts"; // Test workspace conflict detection -Deno.test("addWorkspace: prevents duplicate workspace names", async () => { +test("addWorkspace: prevents duplicate workspace names", async () => { await withTestConfig(async (testConfigDir) => { await clearTestRemotes(testConfigDir); - + // Add first workspace const workspace1 = { name: "test_workspace", remote: "http://localhost:8001/", - workspaceId: "workspace1", + workspaceId: "workspace1", token: "token1" }; - + await addWorkspace(workspace1, { force: true, configDir: testConfigDir }); - + // Try to add workspace with same name but different details const workspace2 = { name: "test_workspace", // Same name @@ -24,33 +24,39 @@ Deno.test("addWorkspace: prevents duplicate workspace names", async () => { workspaceId: "workspace2", // Different ID token: "token2" }; - - // Should throw error in non-interactive mode without force - await assertRejects( - () => addWorkspace(workspace2, { configDir: testConfigDir }), - Error, - "Workspace name conflict. Use --force to overwrite or choose a different name." - ); - + + // Force non-interactive mode so addWorkspace throws instead of prompting + const origStdinTTY = process.stdin.isTTY; + const origStdoutTTY = process.stdout.isTTY; + try { + process.stdin.isTTY = false as any; + process.stdout.isTTY = false as any; + + // Should throw error in non-interactive mode without force + await expect( + addWorkspace(workspace2, { configDir: testConfigDir }) + ).rejects.toThrow("Workspace name conflict. Use --force to overwrite or choose a different name."); + } finally { + process.stdin.isTTY = origStdinTTY; + process.stdout.isTTY = origStdoutTTY; + } + // Should succeed with force flag await addWorkspace(workspace2, { force: true, configDir: testConfigDir }); - + // Verify the workspace was overwritten const workspaces = await allWorkspaces(testConfigDir); - assertEquals(workspaces.length, 1); - assertEquals(workspaces[0].name, "test_workspace"); - assertEquals(workspaces[0].remote, "http://localhost:8002/"); - assertEquals(workspaces[0].workspaceId, "workspace2"); + expect(workspaces.length).toEqual(1); + expect(workspaces[0].name).toEqual("test_workspace"); + expect(workspaces[0].remote).toEqual("http://localhost:8002/"); + expect(workspaces[0].workspaceId).toEqual("workspace2"); }); }); -Deno.test({ - name: "addWorkspace: prevents duplicate (remote, workspaceId) tuples", - ignore: true, // TODO: Investigate addWorkspace behavior - not throwing expected error - fn: async () => { +test("addWorkspace: prevents duplicate (remote, workspaceId) tuples", async () => { await withTestConfig(async (testConfigDir) => { await clearTestRemotes(testConfigDir); - + // Add first workspace const workspace1 = { name: "first_workspace", @@ -58,9 +64,9 @@ Deno.test({ workspaceId: "test", token: "token1" }; - + await addWorkspace(workspace1, { force: true, configDir: testConfigDir }); - + // Try to add workspace with same (remote, workspaceId) but different name const workspace2 = { name: "second_workspace", // Different name @@ -68,30 +74,28 @@ Deno.test({ workspaceId: "test", // Same workspaceId token: "token2" }; - + // Should throw error in non-interactive mode without force - await assertRejects( - () => addWorkspace(workspace2, { configDir: testConfigDir }), - Error, - 'Backend constraint violation: (http://localhost:8001/, test) already exists as "first_workspace". Use --force to overwrite.' - ); - + await expect( + addWorkspace(workspace2, { configDir: testConfigDir }) + ).rejects.toThrow('Backend constraint violation: (http://localhost:8001/, test) already exists as "first_workspace". Use --force to overwrite.'); + // Should succeed with force flag (overwrites first workspace) await addWorkspace(workspace2, { force: true, configDir: testConfigDir }); - + // Verify the first workspace was removed and second was added const workspaces = await allWorkspaces(testConfigDir); - assertEquals(workspaces.length, 1); - assertEquals(workspaces[0].name, "second_workspace"); - assertEquals(workspaces[0].remote, "http://localhost:8001/"); - assertEquals(workspaces[0].workspaceId, "test"); + expect(workspaces.length).toEqual(1); + expect(workspaces[0].name).toEqual("second_workspace"); + expect(workspaces[0].remote).toEqual("http://localhost:8001/"); + expect(workspaces[0].workspaceId).toEqual("test"); }); -}}); +}); -Deno.test("addWorkspace: allows same workspace (name, remote, workspaceId) with token update", async () => { +test("addWorkspace: allows same workspace (name, remote, workspaceId) with token update", async () => { await withTestConfig(async (testConfigDir) => { await clearTestRemotes(testConfigDir); - + // Add first workspace const workspace1 = { name: "same_workspace", @@ -99,9 +103,9 @@ Deno.test("addWorkspace: allows same workspace (name, remote, workspaceId) with workspaceId: "test", token: "old_token" }; - + await addWorkspace(workspace1, { force: true, configDir: testConfigDir }); - + // Add same workspace with updated token const workspace2 = { name: "same_workspace", // Same name @@ -109,19 +113,19 @@ Deno.test("addWorkspace: allows same workspace (name, remote, workspaceId) with workspaceId: "test", // Same workspaceId token: "new_token" // Different token }; - + // Should succeed without force (just token update) await addWorkspace(workspace2, { configDir: testConfigDir }); - + // Verify token was updated const workspaces = await allWorkspaces(testConfigDir); - assertEquals(workspaces.length, 1); - assertEquals(workspaces[0].name, "same_workspace"); - assertEquals(workspaces[0].token, "new_token"); + expect(workspaces.length).toEqual(1); + expect(workspaces[0].name).toEqual("same_workspace"); + expect(workspaces[0].token).toEqual("new_token"); }); }); -Deno.test("addWorkspace: returns true on successful add", async () => { +test("addWorkspace: returns true on successful add", async () => { await withTestConfig(async (testConfigDir) => { await clearTestRemotes(testConfigDir); @@ -133,11 +137,11 @@ Deno.test("addWorkspace: returns true on successful add", async () => { }; const result = await addWorkspace(workspace, { force: true, configDir: testConfigDir }); - assertEquals(result, true); + expect(result).toEqual(true); }); }); -Deno.test("addWorkspace: returns true when force-overwriting conflict", async () => { +test("addWorkspace: returns true when force-overwriting conflict", async () => { await withTestConfig(async (testConfigDir) => { await clearTestRemotes(testConfigDir); @@ -156,14 +160,14 @@ Deno.test("addWorkspace: returns true when force-overwriting conflict", async () token: "token2" }; const result = await addWorkspace(workspace2, { force: true, configDir: testConfigDir }); - assertEquals(result, true); + expect(result).toEqual(true); }); }); -Deno.test("addWorkspace: allows different workspaces on different remotes", async () => { +test("addWorkspace: allows different workspaces on different remotes", async () => { await withTestConfig(async (testConfigDir) => { await clearTestRemotes(testConfigDir); - + // Add workspace on first remote const workspace1 = { name: "workspace_remote1", @@ -171,9 +175,9 @@ Deno.test("addWorkspace: allows different workspaces on different remotes", asyn workspaceId: "test", token: "token1" }; - + await addWorkspace(workspace1, { force: true, configDir: testConfigDir }); - + // Add workspace with same workspaceId on different remote (should be allowed) const workspace2 = { name: "workspace_remote2", @@ -181,15 +185,15 @@ Deno.test("addWorkspace: allows different workspaces on different remotes", asyn workspaceId: "test", // Same workspaceId (OK on different remote) token: "token2" }; - + // Should succeed (different remotes) await addWorkspace(workspace2, { configDir: testConfigDir }); - + // Verify both workspaces exist const workspaces = await allWorkspaces(testConfigDir); - assertEquals(workspaces.length, 2); - + expect(workspaces.length).toEqual(2); + const names = workspaces.map(w => w.name).sort(); - assertEquals(names, ["workspace_remote1", "workspace_remote2"]); + expect(names).toEqual(["workspace_remote1", "workspace_remote2"]); }); -}); \ No newline at end of file +}); diff --git a/cli/test/workspace_deps_filter.test.ts b/cli/test/workspace_deps_filter.test.ts index eab576cd0d..52fa6a7b76 100644 --- a/cli/test/workspace_deps_filter.test.ts +++ b/cli/test/workspace_deps_filter.test.ts @@ -10,11 +10,11 @@ * changing specific deps only marks the expected scripts as stale. */ -import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { expect, test } from "bun:test"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; -import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts"; -import { stringify as stringifyYaml } from "jsr:@std/yaml"; +import { writeFile, mkdir } from "node:fs/promises"; +import { stringify as stringifyYaml } from "@std/yaml"; // Import hash generation utilities from CLI import { generateHash } from "../src/utils/utils.ts"; @@ -45,12 +45,7 @@ function createLockfile(locks: Record): string { // Test 1: Scripts - changing default dep only marks scripts without annotation as stale // ============================================================================= -Deno.test({ - name: "Workspace deps: Scripts - dry-run shows correct stale scripts when default dep changes", - ignore: false, - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Workspace deps: Scripts - dry-run shows correct stale scripts when default dep changes", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -62,20 +57,20 @@ Deno.test({ await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); // Create wmill.yaml - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" -excludes: []`); +excludes: []`, "utf-8"); // Setup dependencies folder (Bun/TypeScript) - await ensureDir(`${tempDir}/dependencies`); + await mkdir(`${tempDir}/dependencies`, { recursive: true }); const defaultDep = `{"dependencies": {"lodash": "4.17.21"}}`; const explicitDep = `{"dependencies": {"axios": "1.6.0"}}`; - await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, defaultDep); - await Deno.writeTextFile(`${tempDir}/dependencies/explicit.package.json`, explicitDep); + await writeFile(`${tempDir}/dependencies/package.json`, defaultDep, "utf-8"); + await writeFile(`${tempDir}/dependencies/explicit.package.json`, explicitDep, "utf-8"); // Setup script folder - await ensureDir(`${tempDir}/f/test`); + await mkdir(`${tempDir}/f/test`, { recursive: true }); // Script 1: No annotation - uses default dep const script1Content = `export async function main() { @@ -88,8 +83,8 @@ schema: properties: {} lock: "" `; - await Deno.writeTextFile(`${tempDir}/f/test/uses_default.ts`, script1Content); - await Deno.writeTextFile(`${tempDir}/f/test/uses_default.script.yaml`, script1Metadata); + await writeFile(`${tempDir}/f/test/uses_default.ts`, script1Content, "utf-8"); + await writeFile(`${tempDir}/f/test/uses_default.script.yaml`, script1Metadata, "utf-8"); // Script 2: Uses explicit dep (TypeScript/Bun with annotation) const script2Content = `// package_json: explicit @@ -103,8 +98,8 @@ schema: properties: {} lock: "" `; - await Deno.writeTextFile(`${tempDir}/f/test/uses_explicit.ts`, script2Content); - await Deno.writeTextFile(`${tempDir}/f/test/uses_explicit.script.yaml`, script2Metadata); + await writeFile(`${tempDir}/f/test/uses_explicit.ts`, script2Content, "utf-8"); + await writeFile(`${tempDir}/f/test/uses_explicit.script.yaml`, script2Metadata, "utf-8"); // Build raw workspace dependencies map (as the CLI would) const rawWorkspaceDeps: Record = { @@ -120,10 +115,10 @@ lock: "" const script2Hash = await generateScriptHash(script2FilteredDeps, script2Content, script2Metadata); // Create initial wmill-lock.yaml with these hashes - await Deno.writeTextFile(`${tempDir}/wmill-lock.yaml`, createLockfile({ + await writeFile(`${tempDir}/wmill-lock.yaml`, createLockfile({ "f/test/uses_default": script1Hash, "f/test/uses_explicit": script2Hash, - })); + }), "utf-8"); // Verify initial state - both scripts should be up-to-date const initialResult = await backend.runCLICommand( @@ -131,13 +126,12 @@ lock: "" tempDir, "workspace_deps_test" ); - assertEquals(initialResult.code, 0, `Initial dry-run should succeed: ${initialResult.stderr}`); - assertStringIncludes(initialResult.stdout, "No metadata to update", - `Initial state should show no updates needed. Output: ${initialResult.stdout}`); + expect(initialResult.code).toEqual(0); + expect(initialResult.stdout).toContain("No metadata to update"); // Now change package.json (default dep) const newDefaultDep = `{"dependencies": {"lodash": "4.17.22"}}`; - await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, newDefaultDep); + await writeFile(`${tempDir}/dependencies/package.json`, newDefaultDep, "utf-8"); // Run dry-run again const afterDefaultChangeResult = await backend.runCLICommand( @@ -145,20 +139,18 @@ lock: "" tempDir, "workspace_deps_test" ); - assertEquals(afterDefaultChangeResult.code, 0, `Dry-run should succeed: ${afterDefaultChangeResult.stderr}`); + expect(afterDefaultChangeResult.code).toEqual(0); // uses_default should be stale (uses default dep which changed) - assertStringIncludes(afterDefaultChangeResult.stdout, "uses_default", - `uses_default should be marked stale after default dep change. Output: ${afterDefaultChangeResult.stdout}`); + expect(afterDefaultChangeResult.stdout).toContain("uses_default"); // uses_explicit should NOT be stale (uses explicit dep, not default) - assert(!afterDefaultChangeResult.stdout.includes("uses_explicit"), - `uses_explicit should NOT be marked stale after default dep change. Output: ${afterDefaultChangeResult.stdout}`); + expect(!afterDefaultChangeResult.stdout.includes("uses_explicit")).toBeTruthy(); // Reset and test the reverse: change explicit dep - await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, defaultDep); // restore original + await writeFile(`${tempDir}/dependencies/package.json`, defaultDep, "utf-8"); // restore original const newExplicitDep = `{"dependencies": {"axios": "1.6.1"}}`; - await Deno.writeTextFile(`${tempDir}/dependencies/explicit.package.json`, newExplicitDep); + await writeFile(`${tempDir}/dependencies/explicit.package.json`, newExplicitDep, "utf-8"); // Run dry-run again const afterExplicitChangeResult = await backend.runCLICommand( @@ -166,29 +158,21 @@ lock: "" tempDir, "workspace_deps_test" ); - assertEquals(afterExplicitChangeResult.code, 0, `Dry-run should succeed: ${afterExplicitChangeResult.stderr}`); + expect(afterExplicitChangeResult.code).toEqual(0); // uses_explicit should be stale (uses explicit dep which changed) - assertStringIncludes(afterExplicitChangeResult.stdout, "uses_explicit", - `uses_explicit should be marked stale after explicit dep change. Output: ${afterExplicitChangeResult.stdout}`); + expect(afterExplicitChangeResult.stdout).toContain("uses_explicit"); // uses_default should NOT be stale (uses default dep, not explicit) - assert(!afterExplicitChangeResult.stdout.includes("uses_default"), - `uses_default should NOT be marked stale after explicit dep change. Output: ${afterExplicitChangeResult.stdout}`); + expect(!afterExplicitChangeResult.stdout.includes("uses_default")).toBeTruthy(); }); - }, -}); + }); // ============================================================================= // Test 2: Flows - filterWorkspaceDependenciesForScripts correctly filters by annotation // ============================================================================= -Deno.test({ - name: "Workspace deps: Flows - filterWorkspaceDependenciesForScripts correctly filters inline scripts", - ignore: false, - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Workspace deps: Flows - filterWorkspaceDependenciesForScripts correctly filters inline scripts", async () => { // This test verifies the filtering logic used by flows without needing workers // We test filterWorkspaceDependenciesForScripts directly since flow generate-locks // doesn't have a --dry-run option @@ -216,21 +200,15 @@ export async function main() { // Filter for default script - should only include default dep const defaultFiltered = filterWorkspaceDependencies(rawWorkspaceDeps, defaultScriptContent, "bun"); - assertEquals(Object.keys(defaultFiltered).length, 1, - `Default script should have 1 filtered dep, got: ${JSON.stringify(defaultFiltered)}`); - assert("dependencies/package.json" in defaultFiltered, - `Default script should have package.json`); - assert(!("dependencies/explicit.package.json" in defaultFiltered), - `Default script should NOT have explicit.package.json`); + expect(Object.keys(defaultFiltered).length).toEqual(1); + expect("dependencies/package.json" in defaultFiltered).toBeTruthy(); + expect(!("dependencies/explicit.package.json" in defaultFiltered)).toBeTruthy(); // Filter for explicit script - should only include explicit dep const explicitFiltered = filterWorkspaceDependencies(rawWorkspaceDeps, explicitScriptContent, "bun"); - assertEquals(Object.keys(explicitFiltered).length, 1, - `Explicit script should have 1 filtered dep, got: ${JSON.stringify(explicitFiltered)}`); - assert("dependencies/explicit.package.json" in explicitFiltered, - `Explicit script should have explicit.package.json`); - assert(!("dependencies/package.json" in explicitFiltered), - `Explicit script should NOT have package.json`); + expect(Object.keys(explicitFiltered).length).toEqual(1); + expect("dependencies/explicit.package.json" in explicitFiltered).toBeTruthy(); + expect(!("dependencies/package.json" in explicitFiltered)).toBeTruthy(); // Verify hashes change correctly when deps change const defaultHash1 = await generateScriptHash(defaultFiltered, defaultScriptContent, "metadata"); @@ -250,12 +228,10 @@ export async function main() { const explicitHash2 = await generateScriptHash(explicitFiltered2, explicitScriptContent, "metadata"); // Default script hash should change (its dep changed) - assert(defaultHash1 !== defaultHash2, - `Default script hash should change when default dep changes`); + expect(defaultHash1 !== defaultHash2).toBeTruthy(); // Explicit script hash should NOT change (its dep didn't change) - assertEquals(explicitHash1, explicitHash2, - `Explicit script hash should NOT change when default dep changes`); + expect(explicitHash1).toEqual(explicitHash2); // Now change explicit dep const newExplicitDep = `{"dependencies": {"axios": "1.6.1"}}`; @@ -271,25 +247,17 @@ export async function main() { const explicitHash3 = await generateScriptHash(explicitFiltered3, explicitScriptContent, "metadata"); // Default script hash should be back to original (dep is back to original) - assertEquals(defaultHash1, defaultHash3, - `Default script hash should be same as original when dep reverts`); + expect(defaultHash1).toEqual(defaultHash3); // Explicit script hash should change (its dep changed) - assert(explicitHash1 !== explicitHash3, - `Explicit script hash should change when explicit dep changes`); - }, -}); + expect(explicitHash1 !== explicitHash3).toBeTruthy(); + }); // ============================================================================= // Test 3: Cross-language isolation - Python dep change doesn't affect Bun script // ============================================================================= -Deno.test({ - name: "Workspace deps: Cross-language - Python dep change doesn't affect Bun script", - ignore: false, - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Workspace deps: Cross-language - Python dep change doesn't affect Bun script", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -301,20 +269,20 @@ Deno.test({ await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); // Create wmill.yaml - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" -excludes: []`); +excludes: []`, "utf-8"); // Setup dependencies folder with deps for multiple languages - await ensureDir(`${tempDir}/dependencies`); + await mkdir(`${tempDir}/dependencies`, { recursive: true }); const pythonDep = "requests==2.31.0"; const bunDep = `{"dependencies": {"lodash": "4.17.21"}}`; - await Deno.writeTextFile(`${tempDir}/dependencies/requirements.in`, pythonDep); - await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, bunDep); + await writeFile(`${tempDir}/dependencies/requirements.in`, pythonDep, "utf-8"); + await writeFile(`${tempDir}/dependencies/package.json`, bunDep, "utf-8"); // Setup script folder - await ensureDir(`${tempDir}/f/test`); + await mkdir(`${tempDir}/f/test`, { recursive: true }); // Python script const pythonContent = `def main(): @@ -326,8 +294,8 @@ schema: properties: {} lock: "" `; - await Deno.writeTextFile(`${tempDir}/f/test/python_script.py`, pythonContent); - await Deno.writeTextFile(`${tempDir}/f/test/python_script.script.yaml`, pythonMetadata); + await writeFile(`${tempDir}/f/test/python_script.py`, pythonContent, "utf-8"); + await writeFile(`${tempDir}/f/test/python_script.script.yaml`, pythonMetadata, "utf-8"); // Bun script const bunContent = `export async function main() { @@ -340,8 +308,8 @@ schema: properties: {} lock: "" `; - await Deno.writeTextFile(`${tempDir}/f/test/bun_script.ts`, bunContent); - await Deno.writeTextFile(`${tempDir}/f/test/bun_script.script.yaml`, bunMetadata); + await writeFile(`${tempDir}/f/test/bun_script.ts`, bunContent, "utf-8"); + await writeFile(`${tempDir}/f/test/bun_script.script.yaml`, bunMetadata, "utf-8"); // Build raw workspace dependencies map const rawWorkspaceDeps: Record = { @@ -354,26 +322,22 @@ lock: "" const bunFilteredDeps = filterWorkspaceDependencies(rawWorkspaceDeps, bunContent, "bun"); // Python script should only get requirements.in - assertEquals(Object.keys(pythonFilteredDeps).length, 1, - `Python script should only have 1 filtered dep, got: ${JSON.stringify(pythonFilteredDeps)}`); - assert("dependencies/requirements.in" in pythonFilteredDeps, - `Python script should have requirements.in in filtered deps`); + expect(Object.keys(pythonFilteredDeps).length).toEqual(1); + expect("dependencies/requirements.in" in pythonFilteredDeps).toBeTruthy(); // Bun script should only get package.json - assertEquals(Object.keys(bunFilteredDeps).length, 1, - `Bun script should only have 1 filtered dep, got: ${JSON.stringify(bunFilteredDeps)}`); - assert("dependencies/package.json" in bunFilteredDeps, - `Bun script should have package.json in filtered deps`); + expect(Object.keys(bunFilteredDeps).length).toEqual(1); + expect("dependencies/package.json" in bunFilteredDeps).toBeTruthy(); // Compute initial hashes const pythonHash = await generateScriptHash(pythonFilteredDeps, pythonContent, pythonMetadata); const bunHash = await generateScriptHash(bunFilteredDeps, bunContent, bunMetadata); // Create initial wmill-lock.yaml - await Deno.writeTextFile(`${tempDir}/wmill-lock.yaml`, createLockfile({ + await writeFile(`${tempDir}/wmill-lock.yaml`, createLockfile({ "f/test/python_script": pythonHash, "f/test/bun_script": bunHash, - })); + }), "utf-8"); // Verify initial state - both scripts should be up-to-date const initialResult = await backend.runCLICommand( @@ -381,12 +345,11 @@ lock: "" tempDir, "workspace_deps_cross_lang_test" ); - assertEquals(initialResult.code, 0, `Initial dry-run should succeed: ${initialResult.stderr}`); - assertStringIncludes(initialResult.stdout, "No metadata to update", - `Initial state should show no updates needed. Output: ${initialResult.stdout}`); + expect(initialResult.code).toEqual(0); + expect(initialResult.stdout).toContain("No metadata to update"); // Change Python dep (requirements.in) - await Deno.writeTextFile(`${tempDir}/dependencies/requirements.in`, "requests==2.32.0"); + await writeFile(`${tempDir}/dependencies/requirements.in`, "requests==2.32.0", "utf-8"); // Run dry-run const afterPythonChangeResult = await backend.runCLICommand( @@ -394,48 +357,38 @@ lock: "" tempDir, "workspace_deps_cross_lang_test" ); - assertEquals(afterPythonChangeResult.code, 0, `Dry-run should succeed: ${afterPythonChangeResult.stderr}`); + expect(afterPythonChangeResult.code).toEqual(0); // python_script should be stale - assertStringIncludes(afterPythonChangeResult.stdout, "python_script", - `python_script should be marked stale after Python dep change. Output: ${afterPythonChangeResult.stdout}`); + expect(afterPythonChangeResult.stdout).toContain("python_script"); // bun_script should NOT be stale (different language) - assert(!afterPythonChangeResult.stdout.includes("bun_script"), - `bun_script should NOT be marked stale after Python dep change. Output: ${afterPythonChangeResult.stdout}`); + expect(!afterPythonChangeResult.stdout.includes("bun_script")).toBeTruthy(); // Reset and test the reverse - await Deno.writeTextFile(`${tempDir}/dependencies/requirements.in`, pythonDep); - await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, `{"dependencies": {"lodash": "4.17.22"}}`); + await writeFile(`${tempDir}/dependencies/requirements.in`, pythonDep, "utf-8"); + await writeFile(`${tempDir}/dependencies/package.json`, `{"dependencies": {"lodash": "4.17.22"}}`, "utf-8"); const afterBunChangeResult = await backend.runCLICommand( ["script", "generate-metadata", "-i", "f/test/python_script*,f/test/bun_script*", "--yes", "--dry-run"], tempDir, "workspace_deps_cross_lang_test" ); - assertEquals(afterBunChangeResult.code, 0, `Dry-run should succeed: ${afterBunChangeResult.stderr}`); + expect(afterBunChangeResult.code).toEqual(0); // bun_script should be stale - assertStringIncludes(afterBunChangeResult.stdout, "bun_script", - `bun_script should be marked stale after Bun dep change. Output: ${afterBunChangeResult.stdout}`); + expect(afterBunChangeResult.stdout).toContain("bun_script"); // python_script should NOT be stale (different language) - assert(!afterBunChangeResult.stdout.includes("python_script"), - `python_script should NOT be marked stale after Bun dep change. Output: ${afterBunChangeResult.stdout}`); + expect(!afterBunChangeResult.stdout.includes("python_script")).toBeTruthy(); }); - }, -}); + }); // ============================================================================= // Test 4: Apps - Create app via API and test filterWorkspaceDependenciesForApp // ============================================================================= -Deno.test({ - name: "Workspace deps: Apps - filterWorkspaceDependenciesForApp with real app via API", - ignore: false, - sanitizeResources: false, - sanitizeOps: false, - fn: async () => { +test("Workspace deps: Apps - filterWorkspaceDependenciesForApp with real app via API", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace const testWorkspace = { @@ -447,10 +400,10 @@ Deno.test({ await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); // Create wmill.yaml - await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" -excludes: []`); +excludes: []`, "utf-8"); // Create app with multiple inline scripts via backend API const appPath = "f/test/multi_script_app"; @@ -530,7 +483,7 @@ excludes: []`); }), } ); - assertEquals(createResponse.ok, true, `Failed to create app: ${await createResponse.text()}`); + expect(createResponse.ok).toEqual(true); // Pull the app to disk const pullResult = await backend.runCLICommand( @@ -538,19 +491,19 @@ excludes: []`); tempDir, "workspace_deps_app_test" ); - assertEquals(pullResult.code, 0, `Sync pull should succeed: ${pullResult.stderr}`); + expect(pullResult.code).toEqual(0); // Setup workspace dependencies - await ensureDir(`${tempDir}/dependencies`); + await mkdir(`${tempDir}/dependencies`, { recursive: true }); const defaultBunDep = `{"dependencies": {"lodash": "4.17.21"}}`; const explicitBunDep = `{"dependencies": {"axios": "1.6.0"}}`; const pythonDep = "requests==2.31.0"; - await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, defaultBunDep); - await Deno.writeTextFile(`${tempDir}/dependencies/explicit.package.json`, explicitBunDep); - await Deno.writeTextFile(`${tempDir}/dependencies/requirements.in`, pythonDep); + await writeFile(`${tempDir}/dependencies/package.json`, defaultBunDep, "utf-8"); + await writeFile(`${tempDir}/dependencies/explicit.package.json`, explicitBunDep, "utf-8"); + await writeFile(`${tempDir}/dependencies/requirements.in`, pythonDep, "utf-8"); // Read the pulled app.yaml - const { yamlParseFile } = await import("../deps.ts"); + const { yamlParseFile } = await import("../src/utils/yaml.ts"); const appFilePath = `${tempDir}/${appPath}.app/app.yaml`; const appFile = await yamlParseFile(appFilePath); @@ -568,14 +521,10 @@ excludes: []`); ); // Verify all 3 dep types are included - assertEquals(Object.keys(filteredDeps).length, 3, - `App with bun (default), bun (explicit), and python should have 3 filtered deps, got: ${JSON.stringify(filteredDeps)}`); - assert("dependencies/package.json" in filteredDeps, - `Should include default package.json for default bun script`); - assert("dependencies/explicit.package.json" in filteredDeps, - `Should include explicit.package.json for annotated bun script`); - assert("dependencies/requirements.in" in filteredDeps, - `Should include requirements.in for python script`); + expect(Object.keys(filteredDeps).length).toEqual(3); + expect("dependencies/package.json" in filteredDeps).toBeTruthy(); + expect("dependencies/explicit.package.json" in filteredDeps).toBeTruthy(); + expect("dependencies/requirements.in" in filteredDeps).toBeTruthy(); // Verify hash changes when deps change const hash1 = await generateHash(JSON.stringify(filteredDeps)); @@ -594,7 +543,6 @@ excludes: []`); ); const hash2 = await generateHash(JSON.stringify(filteredDeps2)); - assert(hash1 !== hash2, `Hash should change when filtered deps change`); + expect(hash1 !== hash2).toBeTruthy(); }); - }, -}); + }); diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 0000000000..60ea163be9 --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": false, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": ["src/**/*", "gen/**/*"], + "exclude": ["node_modules", "dist", "npm", "test"] +} diff --git a/cli/wasm/csharp/windmill_parser_wasm.js b/cli/wasm/csharp/windmill_parser_wasm.js index 5f0003a020..47e8bc20d6 100644 --- a/cli/wasm/csharp/windmill_parser_wasm.js +++ b/cli/wasm/csharp/windmill_parser_wasm.js @@ -103,7 +103,7 @@ const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); let wasmCode = ''; switch (wasm_url.protocol) { case 'file:': - wasmCode = await Deno.readFile(wasm_url); + wasmCode = (await import('node:fs')).readFileSync(wasm_url); break case 'https:': case 'http:': diff --git a/cli/wasm/go/windmill_parser_wasm.js b/cli/wasm/go/windmill_parser_wasm.js index ce2eb507ea..7e49d1d2ab 100644 --- a/cli/wasm/go/windmill_parser_wasm.js +++ b/cli/wasm/go/windmill_parser_wasm.js @@ -103,7 +103,7 @@ const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); let wasmCode = ''; switch (wasm_url.protocol) { case 'file:': - wasmCode = await Deno.readFile(wasm_url); + wasmCode = (await import('node:fs')).readFileSync(wasm_url); break case 'https:': case 'http:': diff --git a/cli/wasm/java/windmill_parser_wasm.js b/cli/wasm/java/windmill_parser_wasm.js index 8dd745d243..c06c35d64f 100644 --- a/cli/wasm/java/windmill_parser_wasm.js +++ b/cli/wasm/java/windmill_parser_wasm.js @@ -103,7 +103,7 @@ const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); let wasmCode = ''; switch (wasm_url.protocol) { case 'file:': - wasmCode = await Deno.readFile(wasm_url); + wasmCode = (await import('node:fs')).readFileSync(wasm_url); break case 'https:': case 'http:': diff --git a/cli/wasm/nu/windmill_parser_wasm.js b/cli/wasm/nu/windmill_parser_wasm.js index 2f21f260da..66c2800995 100644 --- a/cli/wasm/nu/windmill_parser_wasm.js +++ b/cli/wasm/nu/windmill_parser_wasm.js @@ -107,7 +107,7 @@ const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); let wasmCode = ''; switch (wasm_url.protocol) { case 'file:': - wasmCode = await Deno.readFile(wasm_url); + wasmCode = (await import('node:fs')).readFileSync(wasm_url); break case 'https:': case 'http:': diff --git a/cli/wasm/php/windmill_parser_wasm.js b/cli/wasm/php/windmill_parser_wasm.js index e95e3a5126..a73d2f8f59 100644 --- a/cli/wasm/php/windmill_parser_wasm.js +++ b/cli/wasm/php/windmill_parser_wasm.js @@ -114,7 +114,7 @@ const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); let wasmCode = ''; switch (wasm_url.protocol) { case 'file:': - wasmCode = await Deno.readFile(wasm_url); + wasmCode = (await import('node:fs')).readFileSync(wasm_url); break case 'https:': case 'http:': diff --git a/cli/wasm/py/windmill_parser_wasm.js b/cli/wasm/py/windmill_parser_wasm.js index 18c5dffdaa..c940f42556 100644 --- a/cli/wasm/py/windmill_parser_wasm.js +++ b/cli/wasm/py/windmill_parser_wasm.js @@ -133,7 +133,7 @@ const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); let wasmCode = ''; switch (wasm_url.protocol) { case 'file:': - wasmCode = await Deno.readFile(wasm_url); + wasmCode = (await import('node:fs')).readFileSync(wasm_url); break case 'https:': case 'http:': diff --git a/cli/wasm/python/windmill_parser_wasm.js b/cli/wasm/python/windmill_parser_wasm.js index 4eb09bc044..ebf0bdb18d 100644 --- a/cli/wasm/python/windmill_parser_wasm.js +++ b/cli/wasm/python/windmill_parser_wasm.js @@ -129,7 +129,7 @@ const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); let wasmCode = ''; switch (wasm_url.protocol) { case 'file:': - wasmCode = await Deno.readFile(wasm_url); + wasmCode = (await import('node:fs')).readFileSync(wasm_url); break case 'https:': case 'http:': diff --git a/cli/wasm/regex/windmill_parser_wasm.js b/cli/wasm/regex/windmill_parser_wasm.js index 580d3b0334..62c5c662a9 100644 --- a/cli/wasm/regex/windmill_parser_wasm.js +++ b/cli/wasm/regex/windmill_parser_wasm.js @@ -313,7 +313,7 @@ const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); let wasmCode = ''; switch (wasm_url.protocol) { case 'file:': - wasmCode = await Deno.readFile(wasm_url); + wasmCode = (await import('node:fs')).readFileSync(wasm_url); break case 'https:': case 'http:': diff --git a/cli/wasm/ruby/windmill_parser_wasm.js b/cli/wasm/ruby/windmill_parser_wasm.js index 2c44b756b6..ad9cd842cd 100644 --- a/cli/wasm/ruby/windmill_parser_wasm.js +++ b/cli/wasm/ruby/windmill_parser_wasm.js @@ -103,7 +103,7 @@ const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); let wasmCode = ''; switch (wasm_url.protocol) { case 'file:': - wasmCode = await Deno.readFile(wasm_url); + wasmCode = (await import('node:fs')).readFileSync(wasm_url); break case 'https:': case 'http:': diff --git a/cli/wasm/rust/windmill_parser_wasm.js b/cli/wasm/rust/windmill_parser_wasm.js index 6639224cf5..7c2fdd6584 100644 --- a/cli/wasm/rust/windmill_parser_wasm.js +++ b/cli/wasm/rust/windmill_parser_wasm.js @@ -100,7 +100,13 @@ const imports = { }; const wasmUrl = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); -const wasm = (await WebAssembly.instantiateStreaming(fetch(wasmUrl), imports)).instance.exports; +let wasmCode; +if (wasmUrl.protocol === 'file:') { + wasmCode = (await import('node:fs')).readFileSync(wasmUrl); +} else { + wasmCode = await (await fetch(wasmUrl)).arrayBuffer(); +} +const wasm = (await WebAssembly.instantiate(wasmCode, imports)).instance.exports; export { wasm as __wasm }; wasm.__wbindgen_start(); diff --git a/cli/wasm/ts/windmill_parser_wasm.js b/cli/wasm/ts/windmill_parser_wasm.js index 20b7073aac..ba7dcb8de2 100644 --- a/cli/wasm/ts/windmill_parser_wasm.js +++ b/cli/wasm/ts/windmill_parser_wasm.js @@ -432,7 +432,7 @@ const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); let wasmCode = ''; switch (wasm_url.protocol) { case 'file:': - wasmCode = await Deno.readFile(wasm_url); + wasmCode = (await import('node:fs')).readFileSync(wasm_url); break case 'https:': case 'http:': diff --git a/cli/wasm/yaml/windmill_parser_wasm.js b/cli/wasm/yaml/windmill_parser_wasm.js index 909de8a6e8..5b61d05523 100644 --- a/cli/wasm/yaml/windmill_parser_wasm.js +++ b/cli/wasm/yaml/windmill_parser_wasm.js @@ -103,7 +103,7 @@ const wasm_url = new URL('windmill_parser_wasm_bg.wasm', import.meta.url); let wasmCode = ''; switch (wasm_url.protocol) { case 'file:': - wasmCode = await Deno.readFile(wasm_url); + wasmCode = (await import('node:fs')).readFileSync(wasm_url); break case 'https:': case 'http:': diff --git a/cli/windmill-utils-internal/remove-ts-ext.sh b/cli/windmill-utils-internal/remove-ts-ext.sh index 8b5390b73e..b69eb20009 100755 --- a/cli/windmill-utils-internal/remove-ts-ext.sh +++ b/cli/windmill-utils-internal/remove-ts-ext.sh @@ -24,7 +24,8 @@ done if [[ "$RESTORE_MODE" == true ]]; then echo "Adding .ts extensions to imports..." # Only add .ts if the path doesn't already end with .ts or / - REGEX='/\.ts["'\'']/! s/(from|import)[[:space:]]+["'\'']([^"'\'']*[^/])(["'\''])/\1 "\2.ts\3/g' + # Also skip node: built-in module imports + REGEX='/\.ts["'\'']/! { /["'\''"]node:/! s/(from|import)[[:space:]]+["'\'']([^"'\'']*[^/])(["'\''])/\1 "\2.ts\3/g; }' SUCCESS_MSG="✓ All .ts extensions added to import/export statements" else echo "Removing .ts extensions from imports..." diff --git a/cli/windmill-utils-internal/src/config/config.ts b/cli/windmill-utils-internal/src/config/config.ts index d2431e3861..0641607efa 100644 --- a/cli/windmill-utils-internal/src/config/config.ts +++ b/cli/windmill-utils-internal/src/config/config.ts @@ -1,8 +1,4 @@ -// Runtime detection -// @ts-ignore - Cross-platform runtime detection -const isDeno = typeof Deno !== "undefined"; -// @ts-ignore - Cross-platform runtime detection -const isNode = typeof process !== "undefined" && process.versions?.node; +import { stat, mkdir } from "node:fs/promises"; export const WINDMILL_CONFIG_DIR = "windmill"; export const WINDMILL_ACTIVE_WORKSPACE_FILE = "activeWorkspace"; @@ -10,60 +6,22 @@ export const WINDMILL_WORKSPACE_CONFIG_FILE = "remotes.ndjson"; export const INSTANCES_CONFIG_FILE = "instances.ndjson"; export const WINDMILL_ACTIVE_INSTANCE_FILE = "activeInstance"; -// Cross-platform environment variable access function getEnv(key: string): string | undefined { - if (isDeno) { - // @ts-ignore - Deno API - return Deno.env.get(key); - } else { - // @ts-ignore - Node API - return process.env[key]; - } + return process.env[key]; } -// Cross-platform OS detection with normalization function getOS(): "linux" | "darwin" | "windows" | null { - if (isDeno) { - // @ts-ignore - Deno API - return Deno.build.os as "linux" | "darwin" | "windows"; - } else if (isNode) { - // @ts-ignore - Node API - const platform = process.platform; - switch (platform) { - case "linux": return "linux"; - case "darwin": return "darwin"; - case "win32": return "windows"; // Normalize win32 to windows - default: return null; - } - } - return null; -} - -// Cross-platform file system operations -async function stat(path: string | URL): Promise { - if (isDeno) { - // @ts-ignore - Deno API - return await Deno.stat(path); - } else { - // @ts-ignore - Node API - const fs = await import('fs/promises'); - return await fs.stat(path); + const platform = process.platform; + switch (platform) { + case "linux": return "linux"; + case "darwin": return "darwin"; + case "win32": return "windows"; + default: return null; } } -async function mkdir(path: string | URL, options?: { recursive?: boolean }): Promise { - if (isDeno) { - // @ts-ignore - Deno API - await Deno.mkdir(path, options); - } else { - // @ts-ignore - Node API - const fs = await import('fs/promises'); - await fs.mkdir(path, options); - } -} - -function throwIfNotDirectory(fileInfo: any): void { - if (!fileInfo.isDirectory) { +function throwIfNotDirectory(fileInfo: import("node:fs").Stats): void { + if (!fileInfo.isDirectory()) { throw new Error("Path is not a directory"); } } @@ -125,17 +83,8 @@ async function ensureDir(dir: string | URL) { throwIfNotDirectory(fileInfo); return; } catch (err: any) { - // Check for file not found error in cross-platform way - if (isDeno) { - // @ts-ignore - Deno API - if (!(err instanceof Deno.errors.NotFound)) { - throw err; - } - } else { - // Node.js error codes - if (err.code !== 'ENOENT') { - throw err; - } + if (err.code !== 'ENOENT') { + throw err; } } @@ -144,17 +93,8 @@ async function ensureDir(dir: string | URL) { try { await mkdir(dir, { recursive: true }); } catch (err: any) { - // Check for already exists error in cross-platform way - if (isDeno) { - // @ts-ignore - Deno API - if (!(err instanceof Deno.errors.AlreadyExists)) { - throw err; - } - } else { - // Node.js error codes - if (err.code !== 'EEXIST') { - throw err; - } + if (err.code !== 'EEXIST') { + throw err; } const fileInfo = await stat(dir); @@ -163,10 +103,10 @@ async function ensureDir(dir: string | URL) { } export async function getBaseConfigDir(configDirOverride?: string): Promise { - const baseDir = configDirOverride ?? - getEnv("WMILL_CONFIG_DIR") ?? - config_dir() ?? - tmp_dir() ?? + const baseDir = configDirOverride ?? + getEnv("WMILL_CONFIG_DIR") ?? + config_dir() ?? + tmp_dir() ?? "/tmp/"; return baseDir; } @@ -196,4 +136,4 @@ export async function getInstancesConfigFilePath(configDirOverride?: string): Pr export async function getActiveInstanceFilePath(configDirOverride?: string): Promise { const configDir = await getConfigDirPath(configDirOverride); return `${configDir}/${WINDMILL_ACTIVE_INSTANCE_FILE}`; -} \ No newline at end of file +} From 18b3c1ae5c4e8696b81e2959b0cfbfb9ce574ed0 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 21 Feb 2026 21:44:41 +0000 Subject: [PATCH 08/16] nit install dev --- cli/install_dev.sh | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/cli/install_dev.sh b/cli/install_dev.sh index e172ac1288..b552a47352 100755 --- a/cli/install_dev.sh +++ b/cli/install_dev.sh @@ -3,13 +3,30 @@ set -e if [ -z "$1" ]; then - name="wmill" + name="wmill-dev" else name="$1" fi -./gen_wm_client.sh +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +./gen_wm_client.sh ./windmill-utils-internal/gen_wm_client.sh -echo "Installing dev cli as $name (pass arg to override)" -deno install -f -A -g src/main.ts --name $name --unstable \ No newline at end of file +bun install + +INSTALL_DIR="$HOME/.local/bin" +mkdir -p "$INSTALL_DIR" + +cat > "$INSTALL_DIR/$name" < Date: Sat, 21 Feb 2026 22:56:32 +0100 Subject: [PATCH 09/16] fix: make WM_FLOW_PATH available in flow step previews (#8042) * fix: pass flow path in flow step preview for AI agent modules JobLoader.runFlowPreview was missing the path parameter, causing WM_FLOW_PATH to be unavailable when using the Run button on individual flow steps. Test up to here worked correctly because it uses a different code path (utils.svelte.ts) that already passed the path. Co-Authored-By: Claude Opus 4.6 * fix: make WM_FLOW_PATH available for rawscript/script step previews Inject the flow path as `_flow_path` in the job args when running a script preview from the flow editor. The SQL pull queries now use COALESCE to fall back to this arg when no parent runnable path exists, making WM_FLOW_PATH available for individual step "Run" previews. Co-Authored-By: Claude Opus 4.6 * fix: rename _flow_path args key to _FLOW_PATH Match existing convention used by _ENTRYPOINT_OVERRIDE. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- backend/windmill-api/openapi.yaml | 2 ++ backend/windmill-api/src/jobs.rs | 22 ++++++++++++++----- backend/windmill-common/src/worker.rs | 2 +- backend/windmill-queue/src/jobs.rs | 2 +- frontend/src/lib/components/JobLoader.svelte | 12 ++++++---- frontend/src/lib/components/ModuleTest.svelte | 9 +++++--- 6 files changed, 35 insertions(+), 14 deletions(-) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 6b11f791b9..f68c8f2354 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -19671,6 +19671,8 @@ components: type: boolean lock: type: string + flow_path: + type: string required: - args diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 960ad29d71..4fc388ce62 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -42,8 +42,6 @@ use windmill_common::runnable_settings::{ }; #[cfg(feature = "inline_preview")] use windmill_common::runtime_assets::{register_runtime_asset, InsertRuntimeAssetParams}; -use windmill_types::s3::BundleFormat; -use windmill_object_store::upload_artifact_to_store; use windmill_common::scripts::ScriptRunnableSettingsInline; use windmill_common::triggers::TriggerMetadata; use windmill_common::utils::{RunnableKind, WarnAfterExt}; @@ -54,8 +52,10 @@ use windmill_common::workspace_dependencies::{ use windmill_common::DYNAMIC_INPUT_CACHE; #[cfg(all(feature = "enterprise", feature = "smtp"))] use windmill_common::{email_oss::send_email_html, server::load_smtp_config}; +use windmill_object_store::upload_artifact_to_store; #[cfg(feature = "inline_preview")] use windmill_parser::asset_parser::AssetKind; +use windmill_types::s3::BundleFormat; #[cfg(feature = "inline_preview")] use windmill_worker::get_worker_internal_server_inline_utils; @@ -1386,7 +1386,8 @@ async fn get_logs_from_store( log_file_index: &Option>, ) -> Option> { use futures::StreamExt; - let stream = windmill_object_store::get_logs_from_store(log_offset, logs, log_file_index).await?; + let stream = + windmill_object_store::get_logs_from_store(log_offset, logs, log_file_index).await?; let header = bytes::Bytes::from( r#"to remove ansi colors, use: | sed 's/\x1B\[[0-9;]\{1,\}[A-Za-z]//g' "# @@ -2849,6 +2850,7 @@ struct Preview { dedicated_worker: Option, lock: Option, format: Option, + flow_path: Option, } #[cfg(feature = "inline_preview")] @@ -4509,6 +4511,14 @@ async fn run_preview_script( check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?; let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()); + let preview_args = preview.args.unwrap_or_default(); + let flow_path_extra = preview.flow_path.map(|fp| { + let mut extra = HashMap::new(); + extra.insert("_FLOW_PATH".to_string(), to_raw_value(&fp)); + extra + }); + let push_args = PushArgs { extra: flow_path_extra, args: &preview_args }; + let (uuid, tx) = push( &db, tx, @@ -4532,7 +4542,7 @@ async fn run_preview_script( dedicated_worker: preview.dedicated_worker, }), }, - PushArgs::from(&preview.args.unwrap_or_default()), + push_args, authed.display_username(), &authed.email, username_to_permissioned_as(&authed.username), @@ -5772,7 +5782,9 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R #[cfg(all(feature = "enterprise", feature = "parquet"))] if let Some(os) = windmill_object_store::get_object_store().await { let file = os - .get(&windmill_object_store::object_store_reexports::Path::from(format!("logs/{file_p}"))) + .get(&windmill_object_store::object_store_reexports::Path::from( + format!("logs/{file_p}"), + )) .await; if let Ok(file) = file { if let Ok(bytes) = file.bytes().await { diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index f80c78f244..1a2d55b896 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -427,7 +427,7 @@ fn format_pull_query(peek: String) -> String { j.same_worker, j.pre_run_error, j.visible_to_owner, j.tag, j.concurrent_limit, j.concurrency_time_window_s, j.flow_innermost_root_job, j.root_job, j.timeout, j.flow_step_id, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle, j.priority, j.raw_code, j.raw_lock, j.raw_flow, - j.script_entrypoint_override, j.preprocessed, pj.runnable_path as parent_runnable_path, + j.script_entrypoint_override, j.preprocessed, COALESCE(pj.runnable_path, j.args->>'_FLOW_PATH') as parent_runnable_path, COALESCE(p.email, j.permissioned_as_email) as permissioned_as_email, p.username as permissioned_as_username, p.is_admin as permissioned_as_is_admin, p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders, p.end_user_email as permissioned_as_end_user_email FROM q, j diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 0c0d885c7b..6bf162ed22 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -6208,7 +6208,7 @@ pub async fn get_same_worker_job( v2_job.raw_code, v2_job.raw_lock, v2_job.raw_flow, - pj.runnable_path as parent_runnable_path, + COALESCE(pj.runnable_path, v2_job.args->>'_FLOW_PATH') as parent_runnable_path, p.email as permissioned_as_email, p.username as permissioned_as_username, p.is_admin as permissioned_as_is_admin, p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders, p.end_user_email as permissioned_as_end_user_email FROM v2_job_queue diff --git a/frontend/src/lib/components/JobLoader.svelte b/frontend/src/lib/components/JobLoader.svelte index 92e2f858be..fed87bf150 100644 --- a/frontend/src/lib/components/JobLoader.svelte +++ b/frontend/src/lib/components/JobLoader.svelte @@ -226,7 +226,8 @@ export async function runFlowPreview( args: Record, flow: OpenFlow & { tag?: string }, - callbacks?: Callbacks + callbacks?: Callbacks, + path?: string ): Promise { return abstractRun( () => @@ -235,7 +236,8 @@ requestBody: { args, value: flow.value, - tag: flow.tag + tag: flow.tag, + path } }), callbacks @@ -288,7 +290,8 @@ tag: string | undefined, lock?: string, hash?: string, - callbacks?: Callbacks + callbacks?: Callbacks, + flowPath?: string ): Promise { return abstractRun( () => @@ -301,7 +304,8 @@ language: lang as Preview['language'], tag, lock, - script_hash: hash + script_hash: hash, + flow_path: flowPath } }), callbacks diff --git a/frontend/src/lib/components/ModuleTest.svelte b/frontend/src/lib/components/ModuleTest.svelte index ad9d35d113..59c8f973ea 100644 --- a/frontend/src/lib/components/ModuleTest.svelte +++ b/frontend/src/lib/components/ModuleTest.svelte @@ -76,7 +76,8 @@ flowStore?.val?.tag ?? val.tag, undefined, undefined, - callbacks + callbacks, + $pathStore ) } else if (val.type == 'script') { const script = val.hash @@ -90,7 +91,8 @@ flowStore?.val?.tag ?? (val.tag_override ? val.tag_override : script.tag), script.lock, val.hash ?? script.hash, - callbacks + callbacks, + $pathStore ) } else if (val.type == 'flow') { await jobLoader?.runFlowByPath(val.path, args, callbacks) @@ -125,7 +127,8 @@ summary: '', schema }, - callbacks + callbacks, + $pathStore ) } else { throw Error('Not supported module type') From 4fedfdfd11aa8ca7fff6f7aed5ae2b313888f878 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sun, 22 Feb 2026 08:53:28 +0100 Subject: [PATCH 10/16] feat(cli): add consistent get/list/new subcommands for all item types (#8047) * feat(cli): add consistent get/list/new subcommands for all item types Make the CLI consistent so every item type (script, flow, app, resource, resource-type, variable, schedule, folder, trigger) supports get/list/new subcommands, enabling the CLI to be used as a full API client in bash scripts with jq piping. - Add --json flag to all list commands for machine-readable output - Register explicit "list" subcommand alongside default action - Add "get [--json]" subcommand to fetch single items from API - Rename "bootstrap" to "new" for script/flow, keep "bootstrap" as alias - Add "new" subcommand for resource, resource-type, variable, schedule, folder, and trigger to create local template YAML files - Update cli-commands skill documentation for wmill init - Add integration tests for all new commands Co-Authored-By: Claude Opus 4.6 * all * feat: install wmill CLI in Docker images and use it for bash variable/resource access - Install windmill-cli via bun in all Dockerfiles that include bun - DockerfileCli: switch from node:slim to oven/bun:slim - CLI: auto-configure from WM_WORKSPACE/WM_TOKEN/BASE_INTERNAL_URL env vars as last-resort fallback when no workspace is configured - Frontend: replace curl-based bash snippets with wmill variable/resource get - Add backend integration tests for wmill CLI in bash scripts Co-Authored-By: Claude Opus 4.6 * fix(ci): install windmill-cli in backend test workflow Ensures wmill is available on PATH for bash integration tests that use `wmill variable get` and `wmill resource get`. Co-Authored-By: Claude Opus 4.6 * refactor(cli): replace @std/* Deno dependencies with Node.js equivalents Replace @std/log with a lightweight custom logger (core/log.ts), @std/path with node:path, and @std/yaml with the yaml npm package. Also fix process hang on exit, add --node option to install_dev.sh, and add missing hasRequiredPermissions to NpmProvider. Co-Authored-By: Claude Opus 4.6 * all * all * all * refactor(cli): replace @ayonli/jsext and @std/encoding with lightweight alternatives Replace @ayonli/jsext (8.4MB) with tar-stream (32kB) for tar creation, replace @std/encoding with Node.js Buffer.toString("hex"), and fix @windmill-labs/shared-utils to use direct npm instead of JSR mirror. Also resolve merge conflicts in sync.ts and fix pre-existing type errors. Co-Authored-By: Claude Opus 4.6 * fix(cli): use singleQuote YAML output and pass yamlOptions in gitsync pull The yaml library defaults to double quotes, but the codebase (and tests) expect single-quoted strings. Add singleQuote: true to yamlOptions and pass yamlOptions to gitsync-settings pull writeFile calls. Co-Authored-By: Claude Opus 4.6 * all * all * fix(cli): address code review feedback - Install CLI from source in backend tests instead of npm - Fix script bootstrap catch block to re-throw "File already exists" - Add type-safe local variable after trigger kind validation - Use created_by instead of policy.on_behalf_of for app get output - Note --kind is recommended for faster trigger lookup in help text - Document node symlink purpose in Dockerfiles Co-Authored-By: Claude Opus 4.6 * fix(ci): use /usr/bin for wmill wrapper to ensure it's in PATH Co-Authored-By: Claude Opus 4.6 * fix(ci): install wmill to ~/.local/bin to avoid permission issues Co-Authored-By: Claude Opus 4.6 * ci(backend): switch to Blacksmith runner and add cargo caching - Switch from ubicloud-standard-16 to blacksmith-16vcpu-ubuntu-2404 for faster NVMe-backed builds - Add stickydisk for cargo target directory (persistent NVMe cache across runs) - Add cache for cargo registry and git dependencies - Upgrade DuckDB FFI cache from actions/cache@v3 to useblacksmith/cache@v1 - Enable CARGO_INCREMENTAL=1 to benefit from persistent target cache Co-Authored-By: Claude Opus 4.6 * fix ci --------- Co-authored-by: Claude Opus 4.6 --- .github/DockerfileBackendTests | 4 + .github/workflows/backend-test.yml | 29 +- Dockerfile | 4 + backend/tests/fixtures/wmill_cli_test.sql | 10 + backend/tests/worker.rs | 74 + cli/bun.lock | 47 +- cli/install_dev.sh | 34 +- cli/package-lock.json | 1498 +++++++++++++++++ cli/package.json | 13 +- cli/src/commands/app/app.ts | 46 +- cli/src/commands/app/app_metadata.ts | 6 +- cli/src/commands/app/bundle.ts | 2 +- cli/src/commands/app/dev.ts | 4 +- cli/src/commands/app/generate_agents.ts | 2 +- cli/src/commands/app/lint.ts | 2 +- cli/src/commands/app/new.ts | 4 +- cli/src/commands/app/raw_apps.ts | 6 +- cli/src/commands/dependencies/dependencies.ts | 2 +- cli/src/commands/dev/dev.ts | 4 +- cli/src/commands/flow/flow.ts | 64 +- cli/src/commands/flow/flow_metadata.ts | 8 +- cli/src/commands/folder/folder.ts | 86 +- cli/src/commands/gitsync-settings/pull.ts | 11 +- cli/src/commands/gitsync-settings/push.ts | 2 +- cli/src/commands/gitsync-settings/utils.ts | 2 +- cli/src/commands/hub/hub.ts | 2 +- cli/src/commands/init/init.ts | 4 +- cli/src/commands/instance/instance.ts | 6 +- cli/src/commands/jobs/jobs.ts | 2 +- cli/src/commands/lint/lint.ts | 6 +- cli/src/commands/queues/queues.ts | 4 +- .../commands/resource-type/resource-type.ts | 60 +- cli/src/commands/resource/resource.ts | 77 +- cli/src/commands/schedule/schedule.ts | 82 +- cli/src/commands/script/script.ts | 161 +- cli/src/commands/sync/global.ts | 2 +- cli/src/commands/sync/pull.ts | 2 +- cli/src/commands/sync/push.ts | 2 +- cli/src/commands/sync/sync.ts | 29 +- cli/src/commands/trigger/trigger.ts | 247 ++- cli/src/commands/user/user.ts | 4 +- cli/src/commands/variable/variable.ts | 91 +- .../commands/worker-groups/worker-groups.ts | 2 +- cli/src/commands/workers/workers.ts | 2 +- cli/src/commands/workspace/fork.ts | 2 +- cli/src/commands/workspace/workspace.ts | 4 +- cli/src/core/auth.ts | 2 +- cli/src/core/branch-profiles.ts | 2 +- cli/src/core/conf.ts | 6 +- cli/src/core/context.ts | 40 +- cli/src/core/log.ts | 24 + cli/src/core/login.ts | 2 +- cli/src/core/settings.ts | 4 +- cli/src/guidance/skills.ts | 92 +- cli/src/main.ts | 25 +- cli/src/types.ts | 8 +- cli/src/utils/codebase.ts | 2 +- cli/src/utils/git.ts | 2 +- cli/src/utils/metadata.ts | 6 +- cli/src/utils/resource_folders.ts | 4 +- cli/src/utils/tar.ts | 22 + cli/src/utils/upgrade.ts | 4 + cli/src/utils/utils.ts | 7 +- cli/src/utils/yaml.ts | 2 +- cli/test/lint_command.test.ts | 2 +- cli/test/lint_locks.test.ts | 2 +- cli/test/list_get_new_commands.test.ts | 639 +++++++ cli/test/lock_cache.test.ts | 4 +- cli/test/mixed_case_paths.test.ts | 2 +- cli/test/raw_app_sync.test.ts | 2 +- cli/test/sync_pull_push.test.ts | 4 +- cli/test/tar_creation.test.ts | 140 ++ cli/test/wmill_lock.test.ts | 4 +- cli/test/workspace_deps_filter.test.ts | 2 +- .../src/parse/parse-schema.ts | 4 +- docker/DockerfileCli | 8 +- docker/DockerfileSlim | 5 + docker/DockerfileSlimEe | 5 + frontend/package-lock.json | 46 +- frontend/src/lib/components/EditorBar.svelte | 6 +- 80 files changed, 3465 insertions(+), 419 deletions(-) create mode 100644 backend/tests/fixtures/wmill_cli_test.sql create mode 100644 cli/package-lock.json create mode 100644 cli/src/core/log.ts create mode 100644 cli/src/utils/tar.ts create mode 100644 cli/test/list_get_new_commands.test.ts create mode 100644 cli/test/tar_creation.test.ts diff --git a/.github/DockerfileBackendTests b/.github/DockerfileBackendTests index 8783bb241c..b204856823 100644 --- a/.github/DockerfileBackendTests +++ b/.github/DockerfileBackendTests @@ -44,6 +44,10 @@ RUN /usr/local/bin/python3 -m pip install pip-tools # Bun COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun +# Install windmill CLI +RUN bun install -g windmill-cli \ + && ln -s $(bun pm bin -g)/wmill /usr/bin/wmill + ARG TARGETPLATFORM # Deno diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml index 48bd1e11ec..f3a7d1e883 100644 --- a/.github/workflows/backend-test.yml +++ b/.github/workflows/backend-test.yml @@ -19,7 +19,7 @@ defaults: jobs: cargo_test: - runs-on: ubicloud-standard-16 + runs-on: blacksmith-16vcpu-ubuntu-2404 services: postgres: image: postgres @@ -70,6 +70,16 @@ jobs: with: ruby-version: "3.3" bundler-cache: false + - name: Install windmill CLI from source + run: | + cd $GITHUB_WORKSPACE/cli + bash gen_wm_client.sh + bun install + mkdir -p "$HOME/.local/bin" + printf '#!/bin/sh\nexec bun run "%s/cli/src/main.ts" "$@"\n' "$GITHUB_WORKSPACE" > "$HOME/.local/bin/wmill" + chmod +x "$HOME/.local/bin/wmill" + echo "$HOME/.local/bin" >> $GITHUB_PATH + working-directory: / - name: Install PowerShell, mold and clang run: | sudo apt-get update && sudo apt-get install -y powershell mold clang libcurl4-openssl-dev @@ -78,6 +88,20 @@ jobs: with: cache: false toolchain: 1.93.0 + - name: Cache cargo target directory + uses: useblacksmith/stickydisk@v1 + with: + key: cargo-target + path: ./backend/target + - name: Cache cargo registry + uses: useblacksmith/cache@v1 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: cargo-registry-${{ hashFiles('backend/Cargo.lock') }} + restore-keys: | + cargo-registry- - name: Read EE repo commit hash run: | echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV" @@ -205,7 +229,7 @@ jobs: fi echo "Verified: Package requires authentication for @windmill-test/private-pkg" - name: Cache DuckDB FFI module build - uses: actions/cache@v3 + uses: useblacksmith/cache@v1 with: path: ./backend/windmill-duckdb-ffi-internal/target key: ${{ runner.os }}-duckdb-ffi-${{ hashFiles('./backend/windmill-duckdb-ffi-internal/src/**/*.rs', './backend/windmill-duckdb-ffi-internal/Cargo.toml', './backend/windmill-duckdb-ffi-internal/Cargo.lock') }} @@ -221,6 +245,7 @@ jobs: RUST_LOG_STYLE: never CARGO_NET_GIT_FETCH_WITH_CLI: true CARGO_BUILD_JOBS: 12 + CARGO_INCREMENTAL: 1 WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1 WMDEBUG_FORCE_RUNNABLE_SETTINGS_V0: 1 WMDEBUG_FORCE_NO_LEGACY_DEBOUNCING_COMPAT: 1 diff --git a/Dockerfile b/Dockerfile index 16647165db..3b9588a697 100644 --- a/Dockerfile +++ b/Dockerfile @@ -258,6 +258,10 @@ COPY --from=denoland/deno:2.2.1 --chmod=755 /usr/bin/deno /usr/bin/deno COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun +# Install windmill CLI +RUN bun install -g windmill-cli \ + && ln -s $(bun pm bin -g)/wmill /usr/bin/wmill + COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer diff --git a/backend/tests/fixtures/wmill_cli_test.sql b/backend/tests/fixtures/wmill_cli_test.sql new file mode 100644 index 0000000000..e4ac24734b --- /dev/null +++ b/backend/tests/fixtures/wmill_cli_test.sql @@ -0,0 +1,10 @@ +-- Fixture for testing wmill CLI variable/resource get from bash scripts + +INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) +VALUES ('test-workspace', 'u/test-user/test_var', 'hello from variable', false, 'A test variable', '{"u/test-user": true}'); + +INSERT INTO resource_type (workspace_id, name, schema, description, created_by) +VALUES ('test-workspace', 'test_object', '{}', 'Test object type', 'test-user'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/test_res', '{"host": "localhost", "port": 5432}', 'A test resource', 'test_object', '{"u/test-user": true}', 'test-user'); diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 647647604f..9548986a5a 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -993,6 +993,80 @@ echo "hello $msg" Ok(()) } +#[sqlx::test(fixtures("base", "wmill_cli_test"))] +async fn test_bash_wmill_variable_get(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + // The bash script uses wmill CLI to get the variable value. + // The worker sets WM_TOKEN, WM_WORKSPACE, and BASE_INTERNAL_URL as env vars, + // and the CLI auto-configures from them when no workspace is explicitly set. + // We point WMILL_CONFIG_DIR to a clean temp dir so no local active workspace interferes. + let content = r#" +export WMILL_CONFIG_DIR=$(mktemp -d) +result=$(wmill variable get "u/test-user/test_var" --json | jq -r .value) +echo "$result" +"# + .to_owned(); + + let job = RunJob::from(JobPayload::Code(RawCode { + hash: None, + content, + path: None, + lock: None, + language: ScriptLang::Bash, + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + })) + .run_until_complete(&db, false, port) + .await; + assert_eq!(job.json_result(), Some(json!("hello from variable"))); + Ok(()) +} + +#[sqlx::test(fixtures("base", "wmill_cli_test"))] +async fn test_bash_wmill_resource_get(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + // The bash script uses wmill CLI to get the resource value. + // We point WMILL_CONFIG_DIR to a clean temp dir so no local active workspace interferes. + let content = r#" +export WMILL_CONFIG_DIR=$(mktemp -d) +result=$(wmill resource get "u/test-user/test_res" --json | jq -c .value) +echo "$result" +"# + .to_owned(); + + let job = RunJob::from(JobPayload::Code(RawCode { + hash: None, + content, + path: None, + lock: None, + language: ScriptLang::Bash, + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + })) + .run_until_complete(&db, false, port) + .await; + // Bash echo outputs are returned as strings, so the JSON is a string value + assert_eq!( + job.json_result(), + Some(json!("{\"host\":\"localhost\",\"port\":5432}")) + ); + Ok(()) +} + #[cfg(feature = "nu")] #[sqlx::test(fixtures("base"))] async fn test_nu_job(db: Pool) -> anyhow::Result<()> { diff --git a/cli/bun.lock b/cli/bun.lock index 951ba47c7c..7fea1c929b 100644 --- a/cli/bun.lock +++ b/cli/bun.lock @@ -5,16 +5,11 @@ "": { "name": "windmill-cli-dev", "dependencies": { - "@ayonli/jsext": "^1.9.0", "@cliffy/ansi": "npm:@jsr/cliffy__ansi@1.0.0", "@cliffy/command": "npm:@jsr/cliffy__command@1.0.0", "@cliffy/prompt": "npm:@jsr/cliffy__prompt@1.0.0", "@cliffy/table": "npm:@jsr/cliffy__table@1.0.0", - "@std/encoding": "npm:@jsr/std__encoding@1.0.10", - "@std/log": "npm:@jsr/std__log@0.224.14", - "@std/path": "npm:@jsr/std__path@1.1.4", - "@std/yaml": "npm:@jsr/std__yaml@1.0.10", - "@windmill-labs/shared-utils": "npm:@jsr/windmill-labs__shared-utils@1.0.12", + "@windmill-labs/shared-utils": "^1.0.12", "diff": "^5.2.0", "esbuild": "0.24.2", "get-port": "7.1.0", @@ -22,6 +17,7 @@ "minimatch": "^10.0.0", "open": "^10.0.0", "svelte": "^5.45.2", + "tar-stream": "^3.1.7", "windmill-parser-wasm-csharp": "*", "windmill-parser-wasm-go": "*", "windmill-parser-wasm-java": "*", @@ -40,14 +36,13 @@ "devDependencies": { "@types/diff": "^5.2.3", "@types/node": "^22.0.0", + "@types/tar-stream": "^3.1.4", "@types/ws": "^8.5.0", "typescript": "^5.7.0", }, }, }, "packages": { - "@ayonli/jsext": ["@ayonli/jsext@1.9.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "sudo-prompt": "^9.2.1", "ws": "^8.17.0", "zod": "^3.23.8" } }, "sha512-hIu6lQhoLr5e26lmt+vzopuZffaAyb623r4+8HlN/rhXgm2ywHslzk7UHiATdfDbfPjBARkB6cfXjVEi3aav6g=="], - "@cliffy/ansi": ["@jsr/cliffy__ansi@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__ansi/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__internal": "1.0.0", "@jsr/std__encoding": "^1.0.10", "@jsr/std__fmt": "^1.0.9", "@jsr/std__io": "~0.225.3" } }, "sha512-JesgTdgR0aW1mZv96VqvRHr2efzr4MgDFMnoT+hkhaiCpmyBz33sHM5peAoMJUbGVfEfQAsysIXvvgoFYoveYg=="], "@cliffy/command": ["@jsr/cliffy__command@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__command/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__flags": "1.0.0", "@jsr/cliffy__internal": "1.0.0", "@jsr/cliffy__table": "1.0.0", "@jsr/std__fmt": "^1.0.9", "@jsr/std__semver": "^1.0.8", "@jsr/std__text": "^1.0.17" } }, "sha512-oObplVtu1tvpkhgpuPDHZidx9g3axVOfRMQGmw7ZSGxp0+vZIJGiEtpcSvlN0XfuEhOG8neqfVBSSE9txrKanw=="], @@ -134,8 +129,6 @@ "@jsr/std__fmt": ["@jsr/std__fmt@1.0.9", "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.9.tgz", {}, "sha512-YFJJMozmORj2K91c5J9opWeh0VUwrd+Mwb7Pr0FkVCAKVLu2UhT4LyvJqWiyUT+eF+MdfqQ9F7RtQj4bXn9Smw=="], - "@jsr/std__fs": ["@jsr/std__fs@1.0.21", "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.21.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12", "@jsr/std__path": "^1.1.4" } }, "sha512-k/agrcKGm6KD89ci3AEyRmu3wRWf9JZNliOF4ZUxagTHiySmxjiKU3Lk+d2ksRtwEi7oWlLGS0AVM9Lciwc/xg=="], - "@jsr/std__internal": ["@jsr/std__internal@1.0.12", "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz", {}, "sha512-6xReMW9p+paJgqoFRpOE2nogJFvzPfaLHLIlyADYjKMUcwDyjKZxryIbgcU+gxiTygn8yCjld1HoI0ET4/iZeA=="], "@jsr/std__io": ["@jsr/std__io@0.225.3", "https://npm.jsr.io/~/11/@jsr/std__io/0.225.3.tgz", { "dependencies": { "@jsr/std__bytes": "^1.0.6" } }, "sha512-IDXY253ipW6FV34CJVxO+3ubfvSEEzw9N2W303KnLe9K/Y9+v/ID1dQYf9VsCCOFMpFtCmOLqzIZsRqv6yQnWw=="], @@ -148,14 +141,6 @@ "@jsr/std__text": ["@jsr/std__text@1.0.17", "https://npm.jsr.io/~/11/@jsr/std__text/1.0.17.tgz", { "dependencies": { "@jsr/std__regexp": "^1.0.1" } }, "sha512-oZsihl1bcTy1Ixzven8rin8kjChj1zDJWqgpS0oSMGCJDzyB365gtIfAvcMmji+M+FcIWo3goDXfHcFYt+k/kg=="], - "@std/encoding": ["@jsr/std__encoding@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz", {}, "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw=="], - - "@std/log": ["@jsr/std__log@0.224.14", "https://npm.jsr.io/~/11/@jsr/std__log/0.224.14.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.5", "@jsr/std__fs": "^1.0.11", "@jsr/std__io": "^0.225.2" } }, "sha512-EHT7E0plakyzk/gxMrwqUf3YGCCxN3Is25QrEh7toYA7qwj46R4qY7cIaDEKy8QqI5JHOFHwWXOClcPK6goIoQ=="], - - "@std/path": ["@jsr/std__path@1.1.4", "https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12" } }, "sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA=="], - - "@std/yaml": ["@jsr/std__yaml@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__yaml/1.0.10.tgz", {}, "sha512-1WIM023Kvi48pvPE3UO5YcieambLgywUooLhAkkaObIcMB77F/YP2ILdl+vNfik+vElkl9znmuST9AZo8mbCpA=="], - "@stoplight/ordered-object-literal": ["@stoplight/ordered-object-literal@1.0.5", "", {}, "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg=="], "@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="], @@ -174,11 +159,13 @@ "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], + "@types/tar-stream": ["@types/tar-stream@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@windmill-labs/shared-utils": ["@jsr/windmill-labs__shared-utils@1.0.12", "https://npm.jsr.io/~/11/@jsr/windmill-labs__shared-utils/1.0.12.tgz", {}, "sha512-bJOacyfxxNPwNTzA4AxCB5iGFop0h3mCgs+E9j3ZaJYDo1soblY16CebnQ56EPy/M3V344X/QoOFBORyRo1Mnw=="], + "@windmill-labs/shared-utils": ["@windmill-labs/shared-utils@1.0.12", "", {}, "sha512-n68uEYv2B5q2Pp8J9syMS3qPZbppFEfeM7HIBEUfU5lGqi3hwnv4mPvgRUyb6K9im3frXC4gzdIdZdlrDpudXQ=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -188,8 +175,12 @@ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + "b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="], + "balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], + "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -214,14 +205,16 @@ "esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="], + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "get-port": ["get-port@7.1.0", "", {}, "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw=="], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -262,16 +255,18 @@ "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "set-immediate-shim": ["set-immediate-shim@1.0.1", "", {}, "sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ=="], + "streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="], + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "sudo-prompt": ["sudo-prompt@9.2.1", "", {}, "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw=="], - "svelte": ["svelte@5.53.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-WzxFHZhhD23Qzu7JCYdvm1rxvRSzdt9HtHO8TScMBX51bLRFTcJmATVqjqXG+6Ln6hrViGCo9DzwOhAasxwC/w=="], + "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], + + "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -313,7 +308,5 @@ "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], - - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], } } diff --git a/cli/install_dev.sh b/cli/install_dev.sh index b552a47352..ebfa99bd69 100755 --- a/cli/install_dev.sh +++ b/cli/install_dev.sh @@ -2,10 +2,19 @@ set -e -if [ -z "$1" ]; then +# Parse options +USE_NODE=false +name="" +for arg in "$@"; do + case "$arg" in + --node|-node|---node) USE_NODE=true ;; + -*) echo "Unknown option: $arg"; echo "Usage: $0 [name] [--node]"; exit 1 ;; + *) [ -z "$name" ] && name="$arg" ;; + esac +done + +if [ -z "$name" ]; then name="wmill-dev" -else - name="$1" fi SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -18,10 +27,25 @@ bun install INSTALL_DIR="$HOME/.local/bin" mkdir -p "$INSTALL_DIR" -cat > "$INSTALL_DIR/$name" < "$INSTALL_DIR/$name" < "$INSTALL_DIR/$name" <=14.18" + } + }, + "node_modules/@cliffy/ansi": { + "name": "@jsr/cliffy__ansi", + "version": "1.0.0", + "resolved": "https://npm.jsr.io/~/11/@jsr/cliffy__ansi/1.0.0.tgz", + "integrity": "sha512-JesgTdgR0aW1mZv96VqvRHr2efzr4MgDFMnoT+hkhaiCpmyBz33sHM5peAoMJUbGVfEfQAsysIXvvgoFYoveYg==", + "dependencies": { + "@jsr/cliffy__internal": "1.0.0", + "@jsr/std__encoding": "^1.0.10", + "@jsr/std__fmt": "^1.0.9", + "@jsr/std__io": "~0.225.3" + } + }, + "node_modules/@cliffy/command": { + "name": "@jsr/cliffy__command", + "version": "1.0.0", + "resolved": "https://npm.jsr.io/~/11/@jsr/cliffy__command/1.0.0.tgz", + "integrity": "sha512-oObplVtu1tvpkhgpuPDHZidx9g3axVOfRMQGmw7ZSGxp0+vZIJGiEtpcSvlN0XfuEhOG8neqfVBSSE9txrKanw==", + "dependencies": { + "@jsr/cliffy__flags": "1.0.0", + "@jsr/cliffy__internal": "1.0.0", + "@jsr/cliffy__table": "1.0.0", + "@jsr/std__fmt": "^1.0.9", + "@jsr/std__semver": "^1.0.8", + "@jsr/std__text": "^1.0.17" + } + }, + "node_modules/@cliffy/prompt": { + "name": "@jsr/cliffy__prompt", + "version": "1.0.0", + "resolved": "https://npm.jsr.io/~/11/@jsr/cliffy__prompt/1.0.0.tgz", + "integrity": "sha512-JDuHcCAjScV0IUj389brneF6AzJyyP0pK8mymsrGN5/PGQfqK8zr96QpFlo1wmo8BY/3JQAdNfy6NZkPCJ6VWA==", + "dependencies": { + "@jsr/cliffy__ansi": "1.0.0", + "@jsr/cliffy__internal": "1.0.0", + "@jsr/cliffy__keycode": "1.0.0", + "@jsr/std__assert": "^1.0.18", + "@jsr/std__fmt": "^1.0.9", + "@jsr/std__io": "~0.225.3", + "@jsr/std__path": "^1.1.4", + "@jsr/std__text": "^1.0.17" + } + }, + "node_modules/@cliffy/table": { + "name": "@jsr/cliffy__table", + "version": "1.0.0", + "resolved": "https://npm.jsr.io/~/11/@jsr/cliffy__table/1.0.0.tgz", + "integrity": "sha512-VoLxH0DjofHWPWKUc5N+oCwXB6O6e+carnhp23yJTa7qokBb+SCrTIABEgQdIe/p0bxgmZhz17xt2efaAxXvbQ==", + "dependencies": { + "@jsr/std__fmt": "^1.0.9" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsr/cliffy__ansi": { + "version": "1.0.0", + "resolved": "https://npm.jsr.io/~/11/@jsr/cliffy__ansi/1.0.0.tgz", + "integrity": "sha512-JesgTdgR0aW1mZv96VqvRHr2efzr4MgDFMnoT+hkhaiCpmyBz33sHM5peAoMJUbGVfEfQAsysIXvvgoFYoveYg==", + "dependencies": { + "@jsr/cliffy__internal": "1.0.0", + "@jsr/std__encoding": "^1.0.10", + "@jsr/std__fmt": "^1.0.9", + "@jsr/std__io": "~0.225.3" + } + }, + "node_modules/@jsr/cliffy__flags": { + "version": "1.0.0", + "resolved": "https://npm.jsr.io/~/11/@jsr/cliffy__flags/1.0.0.tgz", + "integrity": "sha512-j/v3J8MWu0tkYyisZ2w1HxELxxL/qg6vey9+fRkbTJ+S9J0GeLUn2joouikG7aXpULKCXHTjJ9XH9gQx+F3npw==", + "dependencies": { + "@jsr/cliffy__internal": "1.0.0", + "@jsr/std__text": "^1.0.17" + } + }, + "node_modules/@jsr/cliffy__internal": { + "version": "1.0.0", + "resolved": "https://npm.jsr.io/~/11/@jsr/cliffy__internal/1.0.0.tgz", + "integrity": "sha512-YPkbccbuu+kE55k+nia5jJx5Tu/IolBDXZTAgEA+YRGOzq8I1VkXajwykFXvSbXeVee3zQBU7y0HajVDB7ujQA==", + "dependencies": { + "@jsr/std__fmt": "^1.0.9" + } + }, + "node_modules/@jsr/cliffy__keycode": { + "version": "1.0.0", + "resolved": "https://npm.jsr.io/~/11/@jsr/cliffy__keycode/1.0.0.tgz", + "integrity": "sha512-1ot+y8oZheBTpfgCazWjSOAK2Y2nOQD7NwMuiSAkcRuc1t7VizQZfDpZtBx97NlkYWgjn6ylArt2xyhiyLKRhA==" + }, + "node_modules/@jsr/cliffy__table": { + "version": "1.0.0", + "resolved": "https://npm.jsr.io/~/11/@jsr/cliffy__table/1.0.0.tgz", + "integrity": "sha512-VoLxH0DjofHWPWKUc5N+oCwXB6O6e+carnhp23yJTa7qokBb+SCrTIABEgQdIe/p0bxgmZhz17xt2efaAxXvbQ==", + "dependencies": { + "@jsr/std__fmt": "^1.0.9" + } + }, + "node_modules/@jsr/std__assert": { + "version": "1.0.19", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__assert/1.0.19.tgz", + "integrity": "sha512-pEj6RPkGbqlgRmyKwATp4cUs6+ijxtdrv3bq8v1d2I2CEcMEyPaO8cVKro61wGRDH4cNg8Zx6haztvK/9m7gkA==", + "dependencies": { + "@jsr/std__internal": "^1.0.12" + } + }, + "node_modules/@jsr/std__bytes": { + "version": "1.0.6", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__bytes/1.0.6.tgz", + "integrity": "sha512-St6yKggjFGhxS52IFLJWvkchRFbAKg2Xh8UxA4S1EGz7GJ2Ui+ssDDldj/w2c8vCxvl6qgR0HaYbKeFJNqujmA==" + }, + "node_modules/@jsr/std__encoding": { + "version": "1.0.10", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz", + "integrity": "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw==" + }, + "node_modules/@jsr/std__fmt": { + "version": "1.0.9", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.9.tgz", + "integrity": "sha512-YFJJMozmORj2K91c5J9opWeh0VUwrd+Mwb7Pr0FkVCAKVLu2UhT4LyvJqWiyUT+eF+MdfqQ9F7RtQj4bXn9Smw==" + }, + "node_modules/@jsr/std__fs": { + "version": "1.0.23", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.23.tgz", + "integrity": "sha512-e8jspB3M44E5YhWiLCTqibBBTwVmxQaHN06WvFa/elAKm5E/LfAe8Hj5XGNC8P7a0MIPASlNJsnF1bgO/g+aqg==", + "dependencies": { + "@jsr/std__internal": "^1.0.12", + "@jsr/std__path": "^1.1.4" + } + }, + "node_modules/@jsr/std__internal": { + "version": "1.0.12", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz", + "integrity": "sha512-6xReMW9p+paJgqoFRpOE2nogJFvzPfaLHLIlyADYjKMUcwDyjKZxryIbgcU+gxiTygn8yCjld1HoI0ET4/iZeA==" + }, + "node_modules/@jsr/std__io": { + "version": "0.225.3", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__io/0.225.3.tgz", + "integrity": "sha512-IDXY253ipW6FV34CJVxO+3ubfvSEEzw9N2W303KnLe9K/Y9+v/ID1dQYf9VsCCOFMpFtCmOLqzIZsRqv6yQnWw==", + "dependencies": { + "@jsr/std__bytes": "^1.0.6" + } + }, + "node_modules/@jsr/std__path": { + "version": "1.1.4", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz", + "integrity": "sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA==", + "dependencies": { + "@jsr/std__internal": "^1.0.12" + } + }, + "node_modules/@jsr/std__regexp": { + "version": "1.0.1", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__regexp/1.0.1.tgz", + "integrity": "sha512-AnGeP//DHpPvhCWjI5dR4o013JhCQioD8yMF8drD7PWb0X4kvmO35hbZi+NZhfSolz4Ts2cpPzJY+DUpi2XE9A==" + }, + "node_modules/@jsr/std__semver": { + "version": "1.0.8", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__semver/1.0.8.tgz", + "integrity": "sha512-YhkykPU2Majz66e+rQbP0okYc7kKv+U32aguLPCXZZAL+vEVmBA+khHjPHhLBpWR073gzU3WHqGRgB7a/aXCjg==" + }, + "node_modules/@jsr/std__text": { + "version": "1.0.17", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__text/1.0.17.tgz", + "integrity": "sha512-oZsihl1bcTy1Ixzven8rin8kjChj1zDJWqgpS0oSMGCJDzyB365gtIfAvcMmji+M+FcIWo3goDXfHcFYt+k/kg==", + "dependencies": { + "@jsr/std__regexp": "^1.0.1" + } + }, + "node_modules/@std/encoding": { + "name": "@jsr/std__encoding", + "version": "1.0.10", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz", + "integrity": "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw==" + }, + "node_modules/@std/log": { + "name": "@jsr/std__log", + "version": "0.224.14", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__log/0.224.14.tgz", + "integrity": "sha512-EHT7E0plakyzk/gxMrwqUf3YGCCxN3Is25QrEh7toYA7qwj46R4qY7cIaDEKy8QqI5JHOFHwWXOClcPK6goIoQ==", + "dependencies": { + "@jsr/std__fmt": "^1.0.5", + "@jsr/std__fs": "^1.0.11", + "@jsr/std__io": "^0.225.2" + } + }, + "node_modules/@std/path": { + "name": "@jsr/std__path", + "version": "1.1.4", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz", + "integrity": "sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA==", + "dependencies": { + "@jsr/std__internal": "^1.0.12" + } + }, + "node_modules/@std/yaml": { + "name": "@jsr/std__yaml", + "version": "1.0.10", + "resolved": "https://npm.jsr.io/~/11/@jsr/std__yaml/1.0.10.tgz", + "integrity": "sha512-1WIM023Kvi48pvPE3UO5YcieambLgywUooLhAkkaObIcMB77F/YP2ILdl+vNfik+vElkl9znmuST9AZo8mbCpA==" + }, + "node_modules/@stoplight/ordered-object-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", + "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/types": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz", + "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==", + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.5", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml-ast-parser": "0.0.50", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=10.8" + } + }, + "node_modules/@stoplight/yaml-ast-parser": { + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz", + "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==", + "license": "Apache-2.0" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@types/diff": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz", + "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@windmill-labs/shared-utils": { + "name": "@jsr/windmill-labs__shared-utils", + "version": "1.0.12", + "resolved": "https://npm.jsr.io/~/11/@jsr/windmill-labs__shared-utils/1.0.12.tgz", + "integrity": "sha512-bJOacyfxxNPwNTzA4AxCB5iGFop0h3mCgs+E9j3ZaJYDo1soblY16CebnQ56EPy/M3V344X/QoOFBORyRo1Mnw==" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/devalue": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", + "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", + "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.8.0.tgz", + "integrity": "sha512-cnpQrXvFSLdsR9KR5/x7zdf6c3m8IhZfZzSblFEHSqBaVwD2nvJ4CuCKLyvKvwBgZm08CgfSoiTBQLm5WW9hGw==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/sudo-prompt": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", + "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/svelte": { + "version": "5.53.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.2.tgz", + "integrity": "sha512-yGONuIrcl/BMmqbm6/52Q/NYzfkta7uVlos5NSzGTfNJTTFtPPzra6rAQoQIwAqupeM3s9uuTf5PvioeiCdg9g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.3", + "esm-env": "^1.2.1", + "esrap": "^2.2.2", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/windmill-parser-wasm-csharp": { + "version": "1.510.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-csharp/-/windmill-parser-wasm-csharp-1.510.1.tgz", + "integrity": "sha512-qm09YmnbeYHLwYn1jUnObVzPhYO9NZKMlIO7nlo7zPJBXqksgG5fK/KCtwGw9rChrnz+DsvM9wP5FhrwRLMtwQ==" + }, + "node_modules/windmill-parser-wasm-go": { + "version": "1.510.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-go/-/windmill-parser-wasm-go-1.510.1.tgz", + "integrity": "sha512-HOkk6LXK0wrwvkn+zjm3Gxo90HmyL6TYqmLo2yp8fZuppy7GOngT27zwYeBtwONiPyvDKskzoqPQoEfd8VuUsQ==" + }, + "node_modules/windmill-parser-wasm-java": { + "version": "1.510.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-java/-/windmill-parser-wasm-java-1.510.1.tgz", + "integrity": "sha512-Zle+JZT/ZwUArUVacUudYlS+CaHp2lSnkqD/IhWaRUG+gcv26VbERnrrHPonqXbVMS+eA9ElfXrFM5j0ukaXUw==" + }, + "node_modules/windmill-parser-wasm-nu": { + "version": "1.510.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-nu/-/windmill-parser-wasm-nu-1.510.1.tgz", + "integrity": "sha512-AJLFiUy6af+LpUe7CddDo4+JOmw3c0K/1iOWh8NdTwXcLDj90lL6089mdsVo1apyloLgrTbcuFDzZMXVGBgtCg==" + }, + "node_modules/windmill-parser-wasm-php": { + "version": "1.574.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-php/-/windmill-parser-wasm-php-1.574.1.tgz", + "integrity": "sha512-COyid6B1RYs+bpzUCInsA4HY/WZkpDLfkQ90+AqU/TVTpzYSbAC2JCbIwy0cRElBvlhI4bQ+9Wg6hSQKMpEkpA==" + }, + "node_modules/windmill-parser-wasm-py": { + "version": "1.628.3", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.628.3.tgz", + "integrity": "sha512-TlluqknZpg8cZ+A3m6JFLPseY2PpKtDsxdj26fAnCUzKPtse8TxQR+n0dwC80rfW5TwdWSulvNGRDgcNuf7CTw==" + }, + "node_modules/windmill-parser-wasm-regex": { + "version": "1.639.0", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.639.0.tgz", + "integrity": "sha512-qvYM4sYxB6M0xrqwBljS2fWqOMk6rp++60TRltJnzZDzVaWQrKjTGwNMmfepGAIWy1OGVKp0SCVERhe2P+O6tQ==" + }, + "node_modules/windmill-parser-wasm-ruby": { + "version": "1.526.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-ruby/-/windmill-parser-wasm-ruby-1.526.1.tgz", + "integrity": "sha512-rMBQA8s21wmL2kA5ztRs/ZgVA3ckxe9/NLjxl3iQPL0CX6DlvfaUH0O+AnhpXXDMyBs1Y1SZIhcnbnvsHZ3R8g==" + }, + "node_modules/windmill-parser-wasm-rust": { + "version": "1.558.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-rust/-/windmill-parser-wasm-rust-1.558.1.tgz", + "integrity": "sha512-21S7lm1KF8zO1187rbq14hzPHII2RdM2+D44MoAh1F6VoaScj+Puq0z5B1O/hwn/95R/a9jBlL2D8jbkXtlD1A==" + }, + "node_modules/windmill-parser-wasm-ts": { + "version": "1.623.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.623.1.tgz", + "integrity": "sha512-FBwi/zXxjhZcCvi04oFdNivazru1ynIqSbafHSArfaaBWesBO3nye9UO/WXUlWZm5a7BExbU+3R/eVJrGaornw==" + }, + "node_modules/windmill-parser-wasm-yaml": { + "version": "1.593.0", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-yaml/-/windmill-parser-wasm-yaml-1.593.0.tgz", + "integrity": "sha512-Gyx4aR2jsJYuDrD3mCNTmz7LWOQQXPw5yKNCC1xRgUOPfjsD/tINAFfsBLwVOSmlQQcFZO+wHm4KtDtXOcnGVw==" + }, + "node_modules/windmill-yaml-validator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/windmill-yaml-validator/-/windmill-yaml-validator-1.1.1.tgz", + "integrity": "sha512-CVgAwEoBdJhF39q2N012QffhlGPRIyIWd8gj7NnfG+/lMWgH2k5CBLtKIt6cPF8Bxz+6DGC3st1ARSsecDtbTg==", + "license": "Apache 2.0", + "dependencies": { + "@stoplight/yaml": "^4.3.0", + "ajv": "^8.17.1" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/cli/package.json b/cli/package.json index 2c8df16d77..a07d43a1a4 100644 --- a/cli/package.json +++ b/cli/package.json @@ -13,23 +13,19 @@ "gen-client": "./gen_wm_client.sh && ./windmill-utils-internal/gen_wm_client.sh" }, "dependencies": { - "@ayonli/jsext": "^1.9.0", "@cliffy/ansi": "npm:@jsr/cliffy__ansi@1.0.0", "@cliffy/command": "npm:@jsr/cliffy__command@1.0.0", "@cliffy/prompt": "npm:@jsr/cliffy__prompt@1.0.0", "@cliffy/table": "npm:@jsr/cliffy__table@1.0.0", - "@std/encoding": "npm:@jsr/std__encoding@1.0.10", - "@std/log": "npm:@jsr/std__log@0.224.14", - "@std/path": "npm:@jsr/std__path@1.1.4", - "@std/yaml": "npm:@jsr/std__yaml@1.0.10", - "@windmill-labs/shared-utils": "npm:@jsr/windmill-labs__shared-utils@1.0.12", + "@windmill-labs/shared-utils": "^1.0.12", "diff": "^5.2.0", "esbuild": "0.24.2", - "svelte": "^5.45.2", "get-port": "7.1.0", "jszip": "3.8.0", "minimatch": "^10.0.0", "open": "^10.0.0", + "svelte": "^5.45.2", + "tar-stream": "^3.1.7", "windmill-parser-wasm-csharp": "*", "windmill-parser-wasm-go": "*", "windmill-parser-wasm-java": "*", @@ -47,8 +43,9 @@ }, "devDependencies": { "@types/diff": "^5.2.3", - "@types/ws": "^8.5.0", "@types/node": "^22.0.0", + "@types/tar-stream": "^3.1.4", + "@types/ws": "^8.5.0", "typescript": "^5.7.0" } } diff --git a/cli/src/commands/app/app.ts b/cli/src/commands/app/app.ts index febd55e918..3aa8305542 100644 --- a/cli/src/commands/app/app.ts +++ b/cli/src/commands/app/app.ts @@ -3,8 +3,8 @@ import { resolveWorkspace, validatePath } from "../../core/context.ts"; import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; import * as windmillUtils from "@windmill-labs/shared-utils"; import { yamlParseFile } from "../../utils/yaml.ts"; import * as wmill from "../../../gen/services.gen.ts"; @@ -185,7 +185,7 @@ export async function generatingPolicy( } } -async function list(opts: GlobalOptions & { includeDraftOnly?: boolean }) { +async function list(opts: GlobalOptions & { includeDraftOnly?: boolean; json?: boolean }) { const workspace = await resolveWorkspace(opts); await requireLogin(opts); @@ -206,12 +206,32 @@ async function list(opts: GlobalOptions & { includeDraftOnly?: boolean }) { } } - new Table() - .header(["path", "summary"]) - .padding(2) - .border(true) - .body(total.map((x) => [x.path, x.summary])) - .render(); + if (opts.json) { + console.log(JSON.stringify(total)); + } else { + new Table() + .header(["path", "summary"]) + .padding(2) + .border(true) + .body(total.map((x) => [x.path, x.summary])) + .render(); + } +} + +async function get(opts: GlobalOptions & { json?: boolean }, path: string) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + const a = await wmill.getAppByPath({ + workspace: workspace.workspaceId, + path, + }); + if (opts.json) { + console.log(JSON.stringify(a)); + } else { + console.log(colors.bold("Path:") + " " + a.path); + console.log(colors.bold("Summary:") + " " + (a.summary ?? "")); + console.log(colors.bold("Created by:") + " " + (a.created_by ?? "")); + } } async function push(opts: GlobalOptions, filePath: string, remotePath: string) { @@ -227,7 +247,15 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) { const command = new Command() .description("app related commands") + .option("--json", "Output as JSON (for piping to jq)") .action(list as any) + .command("list", "list all apps") + .option("--json", "Output as JSON (for piping to jq)") + .action(list as any) + .command("get", "get an app's details") + .arguments("") + .option("--json", "Output as JSON (for piping to jq)") + .action(get as any) .command("push", "push a local app ") .arguments(" ") .action(push as any) diff --git a/cli/src/commands/app/app_metadata.ts b/cli/src/commands/app/app_metadata.ts index be0decfa32..3a28770225 100644 --- a/cli/src/commands/app/app_metadata.ts +++ b/cli/src/commands/app/app_metadata.ts @@ -1,10 +1,10 @@ import path from "node:path"; import { readFile, mkdir } from "node:fs/promises"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; import { yamlParseFile } from "../../utils/yaml.ts"; -import { stringify as yamlStringify } from "@std/yaml"; +import { stringify as yamlStringify } from "yaml"; import { GlobalOptions } from "../../types.ts"; import { checkifMetadataUptodate, diff --git a/cli/src/commands/app/bundle.ts b/cli/src/commands/app/bundle.ts index d610d743f2..998546d364 100644 --- a/cli/src/commands/app/bundle.ts +++ b/cli/src/commands/app/bundle.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import process from "node:process"; import { spawn } from "node:child_process"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import { colors } from "@cliffy/ansi/colors"; import * as windmillUtils from "@windmill-labs/shared-utils"; export interface BundleOptions { diff --git a/cli/src/commands/app/dev.ts b/cli/src/commands/app/dev.ts index 7cc106baa4..ad704ee8dd 100644 --- a/cli/src/commands/app/dev.ts +++ b/cli/src/commands/app/dev.ts @@ -1,7 +1,7 @@ import { Command } from "@cliffy/command"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; import * as windmillUtils from "@windmill-labs/shared-utils"; import { yamlParseFile } from "../../utils/yaml.ts"; import * as getPort from "get-port"; diff --git a/cli/src/commands/app/generate_agents.ts b/cli/src/commands/app/generate_agents.ts index f8e34a34d9..eec86a8b19 100644 --- a/cli/src/commands/app/generate_agents.ts +++ b/cli/src/commands/app/generate_agents.ts @@ -5,7 +5,7 @@ import process from "node:process"; import { Command } from "@cliffy/command"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import { yamlParseFile } from "../../utils/yaml.ts"; import { GlobalOptions } from "../../types.ts"; import { resolveWorkspace } from "../../core/context.ts"; diff --git a/cli/src/commands/app/lint.ts b/cli/src/commands/app/lint.ts index 932f714185..12014cc7d5 100644 --- a/cli/src/commands/app/lint.ts +++ b/cli/src/commands/app/lint.ts @@ -3,7 +3,7 @@ import * as path from "node:path"; import process from "node:process"; import { Command } from "@cliffy/command"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import { yamlParseFile } from "../../utils/yaml.ts"; import { GlobalOptions } from "../../types.ts"; import { createBundle } from "./bundle.ts"; diff --git a/cli/src/commands/app/new.ts b/cli/src/commands/app/new.ts index fbbc2b4c36..0ae5065251 100644 --- a/cli/src/commands/app/new.ts +++ b/cli/src/commands/app/new.ts @@ -4,8 +4,8 @@ import { colors } from "@cliffy/ansi/colors"; import { Confirm } from "@cliffy/prompt/confirm"; import { Input } from "@cliffy/prompt/input"; import { Select } from "@cliffy/prompt/select"; -import * as log from "@std/log"; -import { stringify as yamlStringify } from "@std/yaml"; +import * as log from "../../core/log.ts"; +import { stringify as yamlStringify } from "yaml"; import { GlobalOptions } from "../../types.ts"; import { generateAgentsDocumentation, generateDatatablesDocumentation, yamlOptions } from "../sync/sync.ts"; import { resolveWorkspace } from "../../core/context.ts"; diff --git a/cli/src/commands/app/raw_apps.ts b/cli/src/commands/app/raw_apps.ts index 5c8189284a..6e71a3bf30 100644 --- a/cli/src/commands/app/raw_apps.ts +++ b/cli/src/commands/app/raw_apps.ts @@ -1,11 +1,11 @@ import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace, validatePath } from "../../core/context.ts"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; import * as windmillUtils from "@windmill-labs/shared-utils"; import { yamlParseFile } from "../../utils/yaml.ts"; -import { stringify as yamlStringify } from "@std/yaml"; +import { stringify as yamlStringify } from "yaml"; import * as wmill from "../../../gen/services.gen.ts"; import { Policy } from "../../../gen/types.gen.ts"; import path from "node:path"; diff --git a/cli/src/commands/dependencies/dependencies.ts b/cli/src/commands/dependencies/dependencies.ts index aa556974df..9cbcdf86df 100644 --- a/cli/src/commands/dependencies/dependencies.ts +++ b/cli/src/commands/dependencies/dependencies.ts @@ -3,7 +3,7 @@ import { resolveWorkspace } from "../../core/context.ts"; import { GlobalOptions } from "../../types.ts"; import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import * as wmill from "../../../gen/services.gen.ts"; import fs from "node:fs"; import { workspaceDependenciesPathToLanguageAndFilename } from "../../utils/metadata.ts"; diff --git a/cli/src/commands/dev/dev.ts b/cli/src/commands/dev/dev.ts index e0a47f2c69..b2d86cc6ed 100644 --- a/cli/src/commands/dev/dev.ts +++ b/cli/src/commands/dev/dev.ts @@ -1,6 +1,6 @@ import { Command } from "@cliffy/command"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; import { yamlParseFile } from "../../utils/yaml.ts"; import { WebSocket, WebSocketServer } from "ws"; diff --git a/cli/src/commands/flow/flow.ts b/cli/src/commands/flow/flow.ts index 218dc69e8c..eff742ee68 100644 --- a/cli/src/commands/flow/flow.ts +++ b/cli/src/commands/flow/flow.ts @@ -3,9 +3,9 @@ import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; import { Confirm } from "@cliffy/prompt/confirm"; import { Table } from "@cliffy/table"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; -import { stringify as yamlStringify } from "@std/yaml"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; +import { stringify as yamlStringify } from "yaml"; import { yamlParseFile } from "../../utils/yaml.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { readFile } from "node:fs/promises"; @@ -113,7 +113,7 @@ async function push(opts: Options, filePath: string, remotePath: string) { } async function list( - opts: GlobalOptions & { showArchived?: boolean; includeDraftOnly?: boolean } + opts: GlobalOptions & { showArchived?: boolean; includeDraftOnly?: boolean; json?: boolean } ) { const workspace = await resolveWorkspace(opts); await requireLogin(opts); @@ -136,13 +136,35 @@ async function list( } } - new Table() - .header(["path", "summary", "edited by"]) - .padding(2) - .border(true) - .body(total.map((x) => [x.path, x.summary, x.edited_by])) - .render(); + if (opts.json) { + console.log(JSON.stringify(total)); + } else { + new Table() + .header(["path", "summary", "edited by"]) + .padding(2) + .border(true) + .body(total.map((x) => [x.path, x.summary, x.edited_by])) + .render(); + } } +async function get(opts: GlobalOptions & { json?: boolean }, path: string) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + const f = await wmill.getFlowByPath({ + workspace: workspace.workspaceId, + path, + }); + if (opts.json) { + console.log(JSON.stringify(f)); + } else { + console.log(colors.bold("Path:") + " " + f.path); + console.log(colors.bold("Summary:") + " " + (f.summary ?? "")); + console.log(colors.bold("Description:") + " " + (f.description ?? "")); + console.log(colors.bold("Edited by:") + " " + (f.edited_by ?? "")); + console.log(colors.bold("Edited at:") + " " + (f.edited_at ?? "")); + } +} + async function run( opts: GlobalOptions & { data?: string; @@ -375,8 +397,17 @@ export function bootstrap( const command = new Command() .description("flow related commands") - .option("--show-archived", "Enable archived scripts in output") + .option("--show-archived", "Enable archived flows in output") + .option("--json", "Output as JSON (for piping to jq)") .action(list as any) + .command("list", "list all flows") + .option("--show-archived", "Enable archived flows in output") + .option("--json", "Output as JSON (for piping to jq)") + .action(list as any) + .command("get", "get a flow's details") + .arguments("") + .option("--json", "Output as JSON (for piping to jq)") + .action(get as any) .command( "push", "push a local flow spec. This overrides any remote versions." @@ -423,10 +454,15 @@ const command = new Command() "Comma separated patterns to specify which file to NOT take into account." ) .action(generateLocks as any) - .command("bootstrap", "create a new empty flow") + .command("new", "create a new empty flow") .arguments("") - .option("--summary ", "script summary") - .option("--description ", "script description") + .option("--summary ", "flow summary") + .option("--description ", "flow description") + .action(bootstrap as any) + .command("bootstrap", "create a new empty flow (alias for new)") + .arguments("") + .option("--summary ", "flow summary") + .option("--description ", "flow description") .action(bootstrap as any); export default command; diff --git a/cli/src/commands/flow/flow_metadata.ts b/cli/src/commands/flow/flow_metadata.ts index 34771be346..cb2e5336e6 100644 --- a/cli/src/commands/flow/flow_metadata.ts +++ b/cli/src/commands/flow/flow_metadata.ts @@ -1,8 +1,8 @@ import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; -import * as path from "@std/path"; -import { SEPARATOR as SEP } from "@std/path"; -import { stringify as yamlStringify } from "@std/yaml"; +import * as log from "../../core/log.ts"; +import * as path from "node:path"; +import { sep as SEP } from "node:path"; +import { stringify as yamlStringify } from "yaml"; import { yamlParseFile } from "../../utils/yaml.ts"; import { readFile } from "node:fs/promises"; import { GlobalOptions } from "../../types.ts"; diff --git a/cli/src/commands/folder/folder.ts b/cli/src/commands/folder/folder.ts index 1073b2626a..e3967d8ce3 100644 --- a/cli/src/commands/folder/folder.ts +++ b/cli/src/commands/folder/folder.ts @@ -1,10 +1,11 @@ -import { stat } from "node:fs/promises"; +import { stat, writeFile, mkdir } from "node:fs/promises"; +import { stringify as yamlStringify } from "yaml"; import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; import * as wmill from "../../../gen/services.gen.ts"; import { requireLogin } from "../../core/auth.ts"; @@ -18,7 +19,7 @@ export interface FolderFile { display_name: string | undefined; } -async function list(opts: GlobalOptions) { +async function list(opts: GlobalOptions & { json?: boolean }) { const workspace = await resolveWorkspace(opts); await requireLogin(opts); @@ -26,18 +27,60 @@ async function list(opts: GlobalOptions) { workspace: workspace.workspaceId, }); - new Table() - .header(["Name", "Owners", "Extra Perms"]) - .padding(2) - .border(true) - .body( - folders.map((x) => [ - x.name, - x.owners?.join(",") ?? "-", - JSON.stringify(x.extra_perms ?? {}), - ]) - ) - .render(); + if (opts.json) { + console.log(JSON.stringify(folders)); + } else { + new Table() + .header(["Name", "Owners", "Extra Perms"]) + .padding(2) + .border(true) + .body( + folders.map((x) => [ + x.name, + x.owners?.join(",") ?? "-", + JSON.stringify(x.extra_perms ?? {}), + ]) + ) + .render(); + } +} + +async function newFolder(opts: GlobalOptions, name: string) { + const dirPath = `f${SEP}${name}`; + const filePath = `${dirPath}${SEP}folder.meta.yaml`; + try { + await stat(filePath); + throw new Error("File already exists: " + filePath); + } catch (e: any) { + if (e.message?.startsWith("File already exists")) throw e; + } + const template: Omit = { + owners: [], + extra_perms: {}, + }; + await mkdir(dirPath, { recursive: true }); + await writeFile(filePath, yamlStringify(template as Record), { + flag: "wx", + encoding: "utf-8", + }); + log.info(colors.green(`Created ${filePath}`)); +} + +async function get(opts: GlobalOptions & { json?: boolean }, name: string) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + const f = await wmill.getFolder({ + workspace: workspace.workspaceId, + name, + }); + if (opts.json) { + console.log(JSON.stringify(f)); + } else { + console.log(colors.bold("Name:") + " " + f.name); + console.log(colors.bold("Summary:") + " " + (f.summary ?? "")); + console.log(colors.bold("Owners:") + " " + (f.owners?.join(", ") ?? "-")); + console.log(colors.bold("Extra Perms:") + " " + JSON.stringify(f.extra_perms ?? {})); + } } export async function pushFolder( @@ -126,7 +169,18 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) { const command = new Command() .description("folder related commands") + .option("--json", "Output as JSON (for piping to jq)") .action(list as any) + .command("list", "list all folders") + .option("--json", "Output as JSON (for piping to jq)") + .action(list as any) + .command("get", "get a folder's details") + .arguments("") + .option("--json", "Output as JSON (for piping to jq)") + .action(get as any) + .command("new", "create a new folder locally") + .arguments("") + .action(newFolder as any) .command( "push", "push a local folder spec. This overrides any remote versions." diff --git a/cli/src/commands/gitsync-settings/pull.ts b/cli/src/commands/gitsync-settings/pull.ts index 7c58a45bbe..bea37743a4 100644 --- a/cli/src/commands/gitsync-settings/pull.ts +++ b/cli/src/commands/gitsync-settings/pull.ts @@ -1,12 +1,13 @@ import { writeFile } from "node:fs/promises"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; -import { stringify as yamlStringify } from "@std/yaml"; +import * as log from "../../core/log.ts"; +import { stringify as yamlStringify } from "yaml"; import { GlobalOptions } from "../../types.ts"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace } from "../../core/context.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { SyncOptions, readConfigFile, getEffectiveSettings, DEFAULT_SYNC_OPTIONS, getWmillYamlPath } from "../../core/conf.ts"; +import { yamlOptions } from "../sync/sync.ts"; import { deepEqual } from "../../utils/utils.ts"; import { getCurrentGitBranch, isGitRepository } from "../../utils/git.ts"; @@ -176,7 +177,7 @@ export async function pullGitSyncSettings( } // Write the new configuration - await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8"); + await writeFile("wmill.yaml", yamlStringify(updatedConfig, yamlOptions), "utf-8"); if (opts.jsonOutput) { console.log( @@ -372,7 +373,7 @@ export async function pullGitSyncSettings( } // Write updated configuration - await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8"); + await writeFile("wmill.yaml", yamlStringify(updatedConfig, yamlOptions), "utf-8"); if (opts.jsonOutput) { console.log( @@ -449,7 +450,7 @@ export async function pullGitSyncSettings( } // Write updated configuration - await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8"); + await writeFile("wmill.yaml", yamlStringify(updatedConfig, yamlOptions), "utf-8"); if (opts.jsonOutput) { console.log( diff --git a/cli/src/commands/gitsync-settings/push.ts b/cli/src/commands/gitsync-settings/push.ts index 62a3b2781a..0392aa8722 100644 --- a/cli/src/commands/gitsync-settings/push.ts +++ b/cli/src/commands/gitsync-settings/push.ts @@ -1,7 +1,7 @@ import process from "node:process"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import { Confirm } from "@cliffy/prompt/confirm"; import { GlobalOptions } from "../../types.ts"; import { requireLogin } from "../../core/auth.ts"; diff --git a/cli/src/commands/gitsync-settings/utils.ts b/cli/src/commands/gitsync-settings/utils.ts index e2acd5e506..8d255d9f92 100644 --- a/cli/src/commands/gitsync-settings/utils.ts +++ b/cli/src/commands/gitsync-settings/utils.ts @@ -1,5 +1,5 @@ import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import { deepEqual, selectRepository } from "../../utils/utils.ts"; import { SyncOptions, getEffectiveSettings, DEFAULT_SYNC_OPTIONS } from "../../core/conf.ts"; import { GitSyncRepository, GIT_SYNC_FIELDS } from "./types.ts"; diff --git a/cli/src/commands/hub/hub.ts b/cli/src/commands/hub/hub.ts index 51aa59d133..83ec15a5e3 100644 --- a/cli/src/commands/hub/hub.ts +++ b/cli/src/commands/hub/hub.ts @@ -1,5 +1,5 @@ import { Command } from "@cliffy/command"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { requireLogin } from "../../core/auth.ts"; diff --git a/cli/src/commands/init/init.ts b/cli/src/commands/init/init.ts index 81eadbe732..807f2fb31b 100644 --- a/cli/src/commands/init/init.ts +++ b/cli/src/commands/init/init.ts @@ -2,8 +2,8 @@ import { stat, writeFile, rm, mkdir } from "node:fs/promises"; import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; import { Confirm } from "@cliffy/prompt/confirm"; -import * as log from "@std/log"; -import { stringify as yamlStringify } from "@std/yaml"; +import * as log from "../../core/log.ts"; +import { stringify as yamlStringify } from "yaml"; import { GlobalOptions } from "../../types.ts"; import { readLockfile } from "../../utils/metadata.ts"; import { getActiveWorkspaceOrFallback } from "../workspace/workspace.ts"; diff --git a/cli/src/commands/instance/instance.ts b/cli/src/commands/instance/instance.ts index c6b848d379..6b22d49b27 100644 --- a/cli/src/commands/instance/instance.ts +++ b/cli/src/commands/instance/instance.ts @@ -6,9 +6,9 @@ import { Confirm } from "@cliffy/prompt/confirm"; import { Input } from "@cliffy/prompt/input"; import { Select } from "@cliffy/prompt/select"; import { Table } from "@cliffy/table"; -import * as log from "@std/log"; -import * as path from "@std/path"; -import { stringify as yamlStringify } from "@std/yaml"; +import * as log from "../../core/log.ts"; +import * as path from "node:path"; +import { stringify as yamlStringify } from "yaml"; import { setClient } from "../../core/client.ts"; import { yamlParseFile } from "../../utils/yaml.ts"; import * as wmill from "../../../gen/services.gen.ts"; diff --git a/cli/src/commands/jobs/jobs.ts b/cli/src/commands/jobs/jobs.ts index 083426479d..17a58d11f2 100644 --- a/cli/src/commands/jobs/jobs.ts +++ b/cli/src/commands/jobs/jobs.ts @@ -4,7 +4,7 @@ import { resolveWorkspace } from "../../core/context.ts"; import { Command } from "@cliffy/command"; import { colors } from "@cliffy/ansi/colors"; import { Confirm } from "@cliffy/prompt/confirm"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import { mergeConfigWithConfigFile } from "../../core/conf.ts"; import * as fs from "node:fs/promises"; import * as wmill from "../../../gen/services.gen.ts"; diff --git a/cli/src/commands/lint/lint.ts b/cli/src/commands/lint/lint.ts index 6ce244c7c0..62a491eee1 100644 --- a/cli/src/commands/lint/lint.ts +++ b/cli/src/commands/lint/lint.ts @@ -3,9 +3,9 @@ import process from "node:process"; import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; -import * as log from "@std/log"; -import * as path from "@std/path"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../../core/log.ts"; +import * as path from "node:path"; +import { sep as SEP } from "node:path"; import { yamlParseFile } from "../../utils/yaml.ts"; import { GlobalOptions } from "../../types.ts"; import { mergeConfigWithConfigFile } from "../../core/conf.ts"; diff --git a/cli/src/commands/queues/queues.ts b/cli/src/commands/queues/queues.ts index 4f9201a8ab..3f8a18772c 100644 --- a/cli/src/commands/queues/queues.ts +++ b/cli/src/commands/queues/queues.ts @@ -1,6 +1,6 @@ import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { pickInstance } from "../instance/instance.ts"; @@ -124,7 +124,7 @@ async function displayQueues(opts: GlobalOptions, workspace?: string) { table.body(body).render(); } catch (error) { - log.error("Failed to fetch queue metrics:", error); + log.error(`Failed to fetch queue metrics: ${error}`); } } else { log.info("No active instance found"); diff --git a/cli/src/commands/resource-type/resource-type.ts b/cli/src/commands/resource-type/resource-type.ts index 2a4a785ab2..2058c80923 100644 --- a/cli/src/commands/resource-type/resource-type.ts +++ b/cli/src/commands/resource-type/resource-type.ts @@ -1,7 +1,8 @@ import { writeFileSync } from "node:fs"; -import { stat } from "node:fs/promises"; +import { stat, writeFile } from "node:fs/promises"; import path from "node:path"; import process from "node:process"; +import { stringify as yamlStringify } from "yaml"; import { GlobalOptions, @@ -14,7 +15,7 @@ import { resolveWorkspace } from "../../core/context.ts"; import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { ResourceType } from "../../../gen/types.gen.ts"; import { compileResourceTypeToTsType } from "../../utils/resource_types.ts"; @@ -85,14 +86,16 @@ async function push(opts: PushOptions, filePath: string, name: string) { log.info(colors.bold.underline.green("Resource pushed")); } -async function list(opts: GlobalOptions & { schema?: boolean }) { +async function list(opts: GlobalOptions & { schema?: boolean; json?: boolean }) { const workspace = await resolveWorkspace(opts); await requireLogin(opts); const res = await wmill.listResourceType({ workspace: workspace.workspaceId, }); - if (opts.schema) { + if (opts.json) { + console.log(JSON.stringify(res)); + } else if (opts.schema) { new Table() .header(["Workspace", "Name", "Schema"]) .padding(2) @@ -115,6 +118,44 @@ async function list(opts: GlobalOptions & { schema?: boolean }) { } } +async function newResourceType(opts: GlobalOptions, name: string) { + const filePath = name + ".resource-type.yaml"; + try { + await stat(filePath); + throw new Error("File already exists: " + filePath); + } catch (e: any) { + if (e.message?.startsWith("File already exists")) throw e; + } + const template: ResourceTypeFile = { + schema: {}, + description: "", + }; + await writeFile(filePath, yamlStringify(template as Record), { + flag: "wx", + encoding: "utf-8", + }); + log.info(colors.green(`Created ${filePath}`)); +} + +async function get(opts: GlobalOptions & { json?: boolean }, path: string) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + const rt = await wmill.getResourceType({ + workspace: workspace.workspaceId, + path, + }); + if (opts.json) { + console.log(JSON.stringify(rt)); + } else { + console.log(colors.bold("Name:") + " " + rt.name); + console.log(colors.bold("Description:") + " " + (rt.description ?? "")); + console.log(colors.bold("Workspace:") + " " + (rt.workspace_id ?? "Global")); + if (rt.schema) { + console.log(colors.bold("Schema:") + " " + JSON.stringify(rt.schema, null, 2)); + } + } +} + export async function generateRTNamespace(opts: GlobalOptions) { const workspace = await resolveWorkspace(opts); await requireLogin(opts); @@ -146,10 +187,19 @@ export async function generateRTNamespace(opts: GlobalOptions) { const command = new Command() .description("resource type related commands") - .action(() => log.info("2 actions available, list and push.")) + .option("--json", "Output as JSON (for piping to jq)") + .action(list as any) .command("list", "list all resource types") .option("--schema", "Show schema in the output") + .option("--json", "Output as JSON (for piping to jq)") .action(list as any) + .command("get", "get a resource type's details") + .arguments("") + .option("--json", "Output as JSON (for piping to jq)") + .action(get as any) + .command("new", "create a new resource type locally") + .arguments("") + .action(newResourceType as any) .command( "push", "push a local resource spec. This overrides any remote versions." diff --git a/cli/src/commands/resource/resource.ts b/cli/src/commands/resource/resource.ts index 1d16aac782..400130f9b8 100644 --- a/cli/src/commands/resource/resource.ts +++ b/cli/src/commands/resource/resource.ts @@ -1,4 +1,5 @@ -import { stat } from "node:fs/promises"; +import { stat, writeFile } from "node:fs/promises"; +import { stringify as yamlStringify } from "yaml"; import { GlobalOptions, @@ -11,8 +12,8 @@ import { resolveWorkspace, validatePath } from "../../core/context.ts"; import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; import * as wmill from "../../../gen/services.gen.ts"; import { Resource } from "../../../gen/types.gen.ts"; import { readInlinePathSync } from "../../utils/utils.ts"; @@ -131,7 +132,7 @@ async function push(opts: PushOptions, filePath: string, remotePath: string) { log.info(colors.bold.underline.green(`Resource ${remotePath} pushed`)); } -async function list(opts: GlobalOptions) { +async function list(opts: GlobalOptions & { json?: boolean }) { const workspace = await resolveWorkspace(opts); await requireLogin(opts); let page = 0; @@ -150,17 +151,73 @@ async function list(opts: GlobalOptions) { } } - new Table() - .header(["Path", "Resource Type"]) - .padding(2) - .border(true) - .body(total.map((x) => [x.path, x.resource_type])) - .render(); + if (opts.json) { + console.log(JSON.stringify(total)); + } else { + new Table() + .header(["Path", "Resource Type"]) + .padding(2) + .border(true) + .body(total.map((x) => [x.path, x.resource_type])) + .render(); + } +} + +async function newResource(opts: GlobalOptions, path: string) { + if (!validatePath(path)) { + return; + } + const filePath = path + ".resource.yaml"; + try { + await stat(filePath); + throw new Error("File already exists: " + filePath); + } catch (e: any) { + if (e.message?.startsWith("File already exists")) throw e; + // file doesn't exist, proceed + } + const template: ResourceFile = { + value: {}, + resource_type: "", + description: "", + }; + await writeFile(filePath, yamlStringify(template as Record), { + flag: "wx", + encoding: "utf-8", + }); + log.info(colors.green(`Created ${filePath}`)); +} + +async function get(opts: GlobalOptions & { json?: boolean }, path: string) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + const r = await wmill.getResource({ + workspace: workspace.workspaceId, + path, + }); + if (opts.json) { + console.log(JSON.stringify(r)); + } else { + console.log(colors.bold("Path:") + " " + r.path); + console.log(colors.bold("Resource Type:") + " " + (r.resource_type ?? "")); + console.log(colors.bold("Description:") + " " + (r.description ?? "")); + console.log(colors.bold("Value:") + " " + JSON.stringify(r.value, null, 2)); + } } const command = new Command() .description("resource related commands") + .option("--json", "Output as JSON (for piping to jq)") .action(list as any) + .command("list", "list all resources") + .option("--json", "Output as JSON (for piping to jq)") + .action(list as any) + .command("get", "get a resource's details") + .arguments("") + .option("--json", "Output as JSON (for piping to jq)") + .action(get as any) + .command("new", "create a new resource locally") + .arguments("") + .action(newResource as any) .command( "push", "push a local resource spec. This overrides any remote versions." diff --git a/cli/src/commands/schedule/schedule.ts b/cli/src/commands/schedule/schedule.ts index bd5192de66..c8582c5315 100644 --- a/cli/src/commands/schedule/schedule.ts +++ b/cli/src/commands/schedule/schedule.ts @@ -1,10 +1,11 @@ -import { stat } from "node:fs/promises"; +import { stat, writeFile } from "node:fs/promises"; +import { stringify as yamlStringify } from "yaml"; import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace, validatePath } from "../../core/context.ts"; import * as wmill from "../../../gen/services.gen.ts"; @@ -27,7 +28,7 @@ export interface ScheduleFile { enabled: boolean; } -async function list(opts: GlobalOptions) { +async function list(opts: GlobalOptions & { json?: boolean }) { const workspace = await resolveWorkspace(opts); await requireLogin(opts); @@ -35,12 +36,62 @@ async function list(opts: GlobalOptions) { workspace: workspace.workspaceId, }); - new Table() - .header(["Path", "Schedule"]) - .padding(2) - .border(true) - .body(schedules.map((x) => [x.path, x.schedule])) - .render(); + if (opts.json) { + console.log(JSON.stringify(schedules)); + } else { + new Table() + .header(["Path", "Schedule"]) + .padding(2) + .border(true) + .body(schedules.map((x) => [x.path, x.schedule])) + .render(); + } +} + +async function newSchedule(opts: GlobalOptions, path: string) { + if (!validatePath(path)) { + return; + } + const filePath = path + ".schedule.yaml"; + try { + await stat(filePath); + throw new Error("File already exists: " + filePath); + } catch (e: any) { + if (e.message?.startsWith("File already exists")) throw e; + } + const template: ScheduleFile = { + schedule: "0 */6 * * *", + on_failure: "", + script_path: "", + args: {}, + timezone: "Etc/UTC", + is_flow: false, + enabled: false, + }; + await writeFile(filePath, yamlStringify(template as Record), { + flag: "wx", + encoding: "utf-8", + }); + log.info(colors.green(`Created ${filePath}`)); +} + +async function get(opts: GlobalOptions & { json?: boolean }, path: string) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + const s = await wmill.getSchedule({ + workspace: workspace.workspaceId, + path, + }); + if (opts.json) { + console.log(JSON.stringify(s)); + } else { + console.log(colors.bold("Path:") + " " + s.path); + console.log(colors.bold("Schedule:") + " " + s.schedule); + console.log(colors.bold("Timezone:") + " " + (s.timezone ?? "")); + console.log(colors.bold("Script Path:") + " " + (s.script_path ?? "")); + console.log(colors.bold("Is Flow:") + " " + (s.is_flow ? "true" : "false")); + console.log(colors.bold("Enabled:") + " " + (s.enabled ? "true" : "false")); + } } export async function pushSchedule( @@ -137,7 +188,18 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) { const command = new Command() .description("schedule related commands") + .option("--json", "Output as JSON (for piping to jq)") .action(list as any) + .command("list", "list all schedules") + .option("--json", "Output as JSON (for piping to jq)") + .action(list as any) + .command("get", "get a schedule's details") + .arguments("") + .option("--json", "Output as JSON (for piping to jq)") + .action(get as any) + .command("new", "create a new schedule locally") + .arguments("") + .action(newSchedule as any) .command( "push", "push a local schedule spec. This overrides any remote versions." diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index ff91b11fb5..9c6f094b41 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -7,9 +7,9 @@ import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; import { Confirm } from "@cliffy/prompt/confirm"; import { Table } from "@cliffy/table"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; -import { stringify as yamlStringify } from "@std/yaml"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; +import { stringify as yamlStringify } from "yaml"; import { deepEqual } from "../../utils/utils.ts"; import * as wmill from "../../../gen/services.gen.ts"; import * as specificItems from "../../core/specific_items.ts"; @@ -48,7 +48,7 @@ import { } from "../../core/conf.ts"; import { SyncCodebase, listSyncCodebases } from "../../utils/codebase.ts"; import fs from "node:fs"; -import { type Tarball } from "@ayonli/jsext/archive"; +import { createTarBlob, type TarEntry } from "../../utils/tar.ts"; import { execSync } from "node:child_process"; import { NewScript, Script } from "../../../gen/types.gen.ts"; @@ -246,7 +246,7 @@ export async function handleFile( const codebase = language == "bun" ? findCodebase(path, codebases) : undefined; - let bundleContent: string | Tarball | undefined = undefined; + let bundleContent: string | Blob | undefined = undefined; let forceTar = false; if (codebase) { @@ -292,7 +292,6 @@ export async function handleFile( ); } if (outputFiles.length > 1) { - const archiveNpm = await import("@ayonli/jsext/archive"); log.info( `Found multiple output files for ${path}, creating a tarball... ${outputFiles .map((file) => file.path) @@ -300,54 +299,49 @@ export async function handleFile( ); forceTar = true; const startTime = performance.now(); - const tarball = new archiveNpm.Tarball(); const mainPath = path.split(SEP).pop()?.split(".")[0] + ".js"; - const content = + const mainContent = outputFiles.find((file) => file.path == "/" + mainPath)?.text ?? ""; - log.info(`Main content: ${content.length}chars`); - tarball.append(new File([content], "main.js", { type: "text/plain" })); + log.info(`Main content: ${mainContent.length}chars`); + const entries: TarEntry[] = [ + { name: "main.js", content: mainContent }, + ]; for (const file of outputFiles) { if (file.path == "/" + mainPath) { continue; } log.info(`Adding file: ${file.path.substring(1)}`); - - const fil = new File([file.contents as any], file.path.substring(1)); - tarball.append(fil); + entries.push({ name: file.path.substring(1), content: file.contents }); } + bundleContent = await createTarBlob(entries); const endTime = performance.now(); log.info( `Finished creating tarball for ${path}: ${( - tarball.size / 1024 + bundleContent.size / 1024 ).toFixed(0)}kB (${(endTime - startTime).toFixed(0)}ms)` ); - bundleContent = tarball; } else { if (Array.isArray(codebase.assets) && codebase.assets.length > 0) { - const archiveNpm = await import("@ayonli/jsext/archive"); log.info( `Using the following asset configuration for ${path}: ${JSON.stringify( codebase.assets )}` ); const startTime = performance.now(); - const tarball = new archiveNpm.Tarball(); - tarball.append( - new File([bundleContent], "main.js", { type: "text/plain" }) - ); + const entries: TarEntry[] = [ + { name: "main.js", content: bundleContent }, + ]; for (const asset of codebase.assets) { const data = fs.readFileSync(asset.from); - const blob = new Blob([data], { type: "text/plain" }); - const file = new File([blob], asset.to); - tarball.append(file); + entries.push({ name: asset.to, content: data }); } + bundleContent = await createTarBlob(entries); const endTime = performance.now(); log.info( `Finished creating tarball for ${path}: ${( - tarball.size / 1024 + bundleContent.size / 1024 ).toFixed(0)}kB (${(endTime - startTime).toFixed(0)}ms)` ); - bundleContent = tarball; } } } @@ -512,31 +506,8 @@ export async function handleFile( return false; } -async function streamToBlob(stream: ReadableStream): Promise { - // Create a reader from the stream - const reader = stream.getReader(); - const chunks = []; - - // Read the data from the stream - while (true) { - const { done, value } = await reader.read(); - - if (done) { - // If stream is finished, break the loop - break; - } - - // Push the chunk to the array - chunks.push(value); - } - - - const blob = new Blob(chunks as any); - return blob; -} - async function createScript( - bundleContent: string | Tarball | undefined, + bundleContent: string | Blob | undefined, workspaceId: string, body: NewScript, workspace: Workspace @@ -563,7 +534,7 @@ async function createScript( "file", typeof bundleContent == "string" ? bundleContent - : await streamToBlob(bundleContent.stream()) + : bundleContent ); const url = @@ -726,6 +697,7 @@ async function list( showArchived?: boolean; includeWithoutMain?: boolean; includeDraftOnly?: boolean; + json?: boolean; } ) { const workspace = await resolveWorkspace(opts); @@ -750,12 +722,16 @@ async function list( } } - new Table() - .header(["path", "summary", "language", "created by"]) - .padding(2) - .border(true) - .body(total.map((x) => [x.path, x.summary, x.language, x.created_by])) - .render(); + if (opts.json) { + console.log(JSON.stringify(total)); + } else { + new Table() + .header(["path", "summary", "language", "created by"]) + .padding(2) + .border(true) + .body(total.map((x) => [x.path, x.summary, x.language, x.created_by])) + .render(); + } } export async function resolve(input: string): Promise> { @@ -916,6 +892,26 @@ async function show(opts: GlobalOptions, path: string) { log.info(s.content); } +async function get(opts: GlobalOptions & { json?: boolean }, path: string) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + const s = await wmill.getScriptByPath({ + workspace: workspace.workspaceId, + path, + }); + if (opts.json) { + console.log(JSON.stringify(s)); + } else { + console.log(colors.bold("Path:") + " " + s.path); + console.log(colors.bold("Summary:") + " " + (s.summary ?? "")); + console.log(colors.bold("Description:") + " " + (s.description ?? "")); + console.log(colors.bold("Language:") + " " + s.language); + console.log(colors.bold("Kind:") + " " + (s.kind ?? "script")); + console.log(colors.bold("Created by:") + " " + (s.created_by ?? "")); + console.log(colors.bold("Created at:") + " " + (s.created_at ?? "")); + } +} + async function bootstrap( opts: GlobalOptions & { summary: string; description: string }, scriptPath: string, @@ -941,10 +937,15 @@ async function bootstrap( try { await stat(scriptCodeFileFullPath); + throw new Error("File already exists: " + scriptCodeFileFullPath); + } catch (e: any) { + if (e.message?.startsWith("File already exists")) throw e; + } + try { await stat(scriptMetadataFileFullPath); - throw new Error("File already exists in repository"); - } catch { - // file does not exist, we can continue + throw new Error("File already exists: " + scriptMetadataFileFullPath); + } catch (e: any) { + if (e.message?.startsWith("File already exists")) throw e; } const scriptMetadata = defaultScriptMetadata(); @@ -1155,38 +1156,34 @@ async function preview( // Handle multiple output files (create tarball) if (out.outputFiles.length > 1) { - const archiveNpm = await import("@ayonli/jsext/archive"); if (!opts.silent) { log.info(`Creating tarball for multiple output files...`); } - const tarball = new archiveNpm.Tarball(); const mainPath = filePath.split(SEP).pop()?.split(".")[0] + ".js"; const mainContent = out.outputFiles.find((file: OutputFile) => file.path == "/" + mainPath)?.text ?? ""; - tarball.append(new File([mainContent], "main.js", { type: "text/plain" })); + const entries: TarEntry[] = [ + { name: "main.js", content: mainContent }, + ]; for (const file of out.outputFiles) { if (file.path == "/" + mainPath) continue; - - const fil = new File([file.contents as any], file.path.substring(1)); - tarball.append(fil); + entries.push({ name: file.path.substring(1), content: file.contents }); } - bundledContent = await streamToBlob(tarball.stream()); + bundledContent = await createTarBlob(entries); isTar = true; } else if (Array.isArray(codebase.assets) && codebase.assets.length > 0) { // Handle assets - const archiveNpm = await import("@ayonli/jsext/archive"); if (!opts.silent) { log.info(`Adding assets to tarball...`); } - const tarball = new archiveNpm.Tarball(); - tarball.append(new File([bundledContent], "main.js", { type: "text/plain" })); + const entries: TarEntry[] = [ + { name: "main.js", content: bundledContent }, + ]; for (const asset of codebase.assets) { const data = fs.readFileSync(asset.from); - const blob = new Blob([data], { type: "text/plain" }); - const file = new File([blob], asset.to); - tarball.append(file); + entries.push({ name: asset.to, content: data }); } - bundledContent = await streamToBlob(tarball.stream()); + bundledContent = await createTarBlob(entries); isTar = true; } @@ -1290,6 +1287,11 @@ async function preview( const command = new Command() .description("script related commands") .option("--show-archived", "Enable archived scripts in output") + .option("--json", "Output as JSON (for piping to jq)") + .action(list as any) + .command("list", "list all scripts") + .option("--show-archived", "Enable archived scripts in output") + .option("--json", "Output as JSON (for piping to jq)") .action(list as any) .command( "push", @@ -1297,7 +1299,11 @@ const command = new Command() ) .arguments("") .action(push as any) - .command("show", "show a scripts content") + .command("get", "get a script's details") + .arguments("") + .option("--json", "Output as JSON (for piping to jq)") + .action(get as any) + .command("show", "show a script's content (alias for get)") .arguments("") .action(show as any) .command("run", "run a script by path") @@ -1325,7 +1331,12 @@ const command = new Command() "Do not output anything other than the final output. Useful for scripting." ) .action(preview as any) - .command("bootstrap", "create a new script") + .command("new", "create a new script") + .arguments(" ") + .option("--summary ", "script summary") + .option("--description ", "script description") + .action(bootstrap as any) + .command("bootstrap", "create a new script (alias for new)") .arguments(" ") .option("--summary ", "script summary") .option("--description ", "script description") diff --git a/cli/src/commands/sync/global.ts b/cli/src/commands/sync/global.ts index 859dab98b8..b30848942a 100644 --- a/cli/src/commands/sync/global.ts +++ b/cli/src/commands/sync/global.ts @@ -1,5 +1,5 @@ import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; let GLOBAL_VERSIONS: { remoteMajor: number | undefined; diff --git a/cli/src/commands/sync/pull.ts b/cli/src/commands/sync/pull.ts index 54ddfdfa15..2d72bc35cf 100644 --- a/cli/src/commands/sync/pull.ts +++ b/cli/src/commands/sync/pull.ts @@ -1,7 +1,7 @@ import { GlobalOptions } from "../../types.ts"; import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import JSZip from "jszip"; import { Workspace } from "../workspace/workspace.ts"; import { getHeaders } from "../../utils/utils.ts"; diff --git a/cli/src/commands/sync/push.ts b/cli/src/commands/sync/push.ts index a62150d59e..e95fa048be 100644 --- a/cli/src/commands/sync/push.ts +++ b/cli/src/commands/sync/push.ts @@ -1,6 +1,6 @@ import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import { GlobalOptions } from "../../types.ts"; function stub(_opts: GlobalOptions, _dir?: string) { diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index ca5c4ce6d2..bf2ecf7826 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -4,10 +4,10 @@ import { readFile, writeFile, readdir, stat, rm, copyFile, mkdir } from "node:fs import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; import { Confirm } from "@cliffy/prompt/confirm"; -import * as log from "@std/log"; -import * as path from "@std/path"; -import { SEPARATOR as SEP } from "@std/path"; -import { stringify as yamlStringify } from "@std/yaml"; +import * as log from "../../core/log.ts"; +import * as path from "node:path"; +import { sep as SEP } from "node:path"; +import { stringify as yamlStringify, type DocumentOptions, type SchemaOptions, type CreateNodeOptions, type ToStringOptions } from "yaml"; import JSZip from "jszip"; import { minimatch } from "minimatch"; import { yamlParseContent } from "../../utils/yaml.ts"; @@ -276,13 +276,12 @@ function prioritizeName(name: string): string { return name; } -export const yamlOptions = { - sortKeys: (a: any, b: any) => { - return prioritizeName(a).localeCompare(prioritizeName(b)); +export const yamlOptions: DocumentOptions & SchemaOptions & CreateNodeOptions & ToStringOptions = { + sortMapEntries: (a, b) => { + return prioritizeName(String(a.key)).localeCompare(prioritizeName(String(b.key))); }, - noCompatMode: true, - noRefs: true, - skipInvalid: true, + aliasDuplicateObjects: false, + singleQuote: true, }; export interface InlineScript { @@ -1338,17 +1337,19 @@ async function compareDynFSElement( continue; } if (!ignoreCodebaseChanges) { + const beforeCodebase = before?.codebase; + const afterCodebase = after?.codebase; if (before?.codebase != undefined) { delete before.codebase; m2[k] = yamlStringify(before, yamlOptions); } if (after?.codebase != undefined) { - if (before.codebase != after.codebase) { - codebaseChanges[k] = after.codebase; - } delete after.codebase; v = yamlStringify(after, yamlOptions); } + if (beforeCodebase != afterCodebase) { + codebaseChanges[k] = afterCodebase ?? beforeCodebase ?? ""; + } } if (skipMetadata) { continue; @@ -2214,7 +2215,7 @@ export async function push( `\nPush aborted: ${lockIssues.length} script(s) missing locks.`, ), ); - Deno.exit(1); + process.exit(1); } log.info(colors.green("All scripts have valid locks.")); } diff --git a/cli/src/commands/trigger/trigger.ts b/cli/src/commands/trigger/trigger.ts index 5e4c8e234a..be645a83fa 100644 --- a/cli/src/commands/trigger/trigger.ts +++ b/cli/src/commands/trigger/trigger.ts @@ -1,4 +1,5 @@ -import { stat } from "node:fs/promises"; +import { stat, writeFile } from "node:fs/promises"; +import { stringify as yamlStringify } from "yaml"; import * as wmill from "../../../gen/services.gen.ts"; import { @@ -18,8 +19,8 @@ import { import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; import { GlobalOptions, isSuperset, @@ -295,37 +296,192 @@ export async function pushNativeTrigger( } } -async function list(opts: GlobalOptions) { +const triggerTemplates: Record> = { + http: { + script_path: "", + is_flow: false, + route_path: "", + http_method: "get", + is_async: false, + requires_auth: true, + }, + websocket: { + script_path: "", + is_flow: false, + url: "", + enabled: false, + }, + kafka: { + script_path: "", + is_flow: false, + kafka_resource_path: "", + group_id: "", + topics: [], + enabled: false, + }, + nats: { + script_path: "", + is_flow: false, + nats_resource_path: "", + subjects: [], + enabled: false, + }, + postgres: { + script_path: "", + is_flow: false, + postgres_resource_path: "", + publication_name: "", + replication_slot_name: "", + enabled: false, + }, + mqtt: { + script_path: "", + is_flow: false, + mqtt_resource_path: "", + topics: [], + subscribe_qos: 0, + enabled: false, + }, + sqs: { + script_path: "", + is_flow: false, + sqs_resource_path: "", + queue_url: "", + enabled: false, + }, + gcp: { + script_path: "", + is_flow: false, + gcp_resource_path: "", + subscription_id: "", + topic_id: "", + enabled: false, + }, + email: { + script_path: "", + is_flow: false, + enabled: false, + }, +}; + +async function newTrigger(opts: GlobalOptions & { kind: string }, path: string) { + if (!validatePath(path)) { + return; + } + if (!opts.kind) { + throw new Error("--kind is required. Valid kinds: " + TRIGGER_TYPES.join(", ")); + } + if (!checkIfValidTrigger(opts.kind)) { + throw new Error("Invalid trigger kind: " + opts.kind + ". Valid kinds: " + TRIGGER_TYPES.join(", ")); + } + const kind: TriggerType = opts.kind; + const filePath = `${path}.${kind}_trigger.yaml`; + try { + await stat(filePath); + throw new Error("File already exists: " + filePath); + } catch (e: any) { + if (e.message?.startsWith("File already exists")) throw e; + } + const template = triggerTemplates[kind]; + await writeFile(filePath, yamlStringify(template), { + flag: "wx", + encoding: "utf-8", + }); + log.info(colors.green(`Created ${filePath}`)); +} + +async function get(opts: GlobalOptions & { json?: boolean; kind?: string }, path: string) { const workspace = await resolveWorkspace(opts); await requireLogin(opts); - const httpTriggers = await wmill.listHttpTriggers({ - workspace: workspace.workspaceId, - }); - const websocketTriggers = await wmill.listWebsocketTriggers({ - workspace: workspace.workspaceId, - }); - const kafkaTriggers = await wmill.listKafkaTriggers({ - workspace: workspace.workspaceId, - }); - const natsTriggers = await wmill.listNatsTriggers({ - workspace: workspace.workspaceId, - }); - const postgresTriggers = await wmill.listPostgresTriggers({ - workspace: workspace.workspaceId, - }); - const mqttTriggers = await wmill.listMqttTriggers({ - workspace: workspace.workspaceId, - }); - const sqsTriggers = await wmill.listSqsTriggers({ - workspace: workspace.workspaceId, - }); - const gcpTriggers = await wmill.listGcpTriggers({ - workspace: workspace.workspaceId, - }); - const emailTriggers = await wmill.listEmailTriggers({ - workspace: workspace.workspaceId, - }); + if (opts.kind) { + if (!checkIfValidTrigger(opts.kind)) { + throw new Error("Invalid trigger kind: " + opts.kind + ". Valid kinds: " + TRIGGER_TYPES.join(", ")); + } + const trigger = await getTrigger(opts.kind, workspace.workspaceId, path); + if (opts.json) { + console.log(JSON.stringify(trigger)); + } else { + console.log(colors.bold("Path:") + " " + (trigger as any).path); + console.log(colors.bold("Kind:") + " " + opts.kind); + console.log(colors.bold("Enabled:") + " " + ((trigger as any).enabled ?? "-")); + console.log(colors.bold("Script Path:") + " " + ((trigger as any).script_path ?? "")); + console.log(colors.bold("Is Flow:") + " " + ((trigger as any).is_flow ? "true" : "false")); + } + return; + } + + // Try all trigger types and collect matches + const matches: { kind: string; trigger: any }[] = []; + for (const kind of TRIGGER_TYPES) { + try { + const trigger = await getTrigger(kind, workspace.workspaceId, path); + matches.push({ kind, trigger }); + } catch { + // not found for this kind + } + } + + if (matches.length === 0) { + throw new Error("No trigger found at path: " + path); + } + + if (matches.length === 1) { + const { kind, trigger } = matches[0]; + if (opts.json) { + console.log(JSON.stringify(trigger)); + } else { + console.log(colors.bold("Path:") + " " + trigger.path); + console.log(colors.bold("Kind:") + " " + kind); + console.log(colors.bold("Enabled:") + " " + (trigger.enabled ?? "-")); + console.log(colors.bold("Script Path:") + " " + (trigger.script_path ?? "")); + console.log(colors.bold("Is Flow:") + " " + (trigger.is_flow ? "true" : "false")); + } + return; + } + + // Multiple matches — ask user to specify --kind + console.log("Multiple triggers found at path " + path + ":"); + for (const m of matches) { + console.log(" - " + m.kind); + } + console.log("Please specify --kind to select one."); +} + +async function listOrEmpty(fn: () => Promise): Promise { + try { + return await fn(); + } catch { + return []; + } +} + +async function list(opts: GlobalOptions & { json?: boolean }) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + + const ws = workspace.workspaceId; + const [ + httpTriggers, + websocketTriggers, + kafkaTriggers, + natsTriggers, + postgresTriggers, + mqttTriggers, + sqsTriggers, + gcpTriggers, + emailTriggers, + ] = await Promise.all([ + listOrEmpty(() => wmill.listHttpTriggers({ workspace: ws })), + listOrEmpty(() => wmill.listWebsocketTriggers({ workspace: ws })), + listOrEmpty(() => wmill.listKafkaTriggers({ workspace: ws })), + listOrEmpty(() => wmill.listNatsTriggers({ workspace: ws })), + listOrEmpty(() => wmill.listPostgresTriggers({ workspace: ws })), + listOrEmpty(() => wmill.listMqttTriggers({ workspace: ws })), + listOrEmpty(() => wmill.listSqsTriggers({ workspace: ws })), + listOrEmpty(() => wmill.listGcpTriggers({ workspace: ws })), + listOrEmpty(() => wmill.listEmailTriggers({ workspace: ws })), + ]); const triggers = [ ...httpTriggers.map((x) => ({ path: x.path, kind: "http" })), ...websocketTriggers.map((x) => ({ path: x.path, kind: "websocket" })), @@ -338,12 +494,16 @@ async function list(opts: GlobalOptions) { ...emailTriggers.map((x) => ({ path: x.path, kind: "email" })), ]; - new Table() - .header(["Path", "Kind"]) - .padding(2) - .border(true) - .body(triggers.map((x) => [x.path, x.kind])) - .render(); + if (opts.json) { + console.log(JSON.stringify(triggers)); + } else { + new Table() + .header(["Path", "Kind"]) + .padding(2) + .border(true) + .body(triggers.map((x) => [x.path, x.kind])) + .render(); + } } function checkIfValidTrigger(kind: string | undefined): kind is TriggerType { @@ -401,7 +561,20 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) { const command = new Command() .description("trigger related commands") + .option("--json", "Output as JSON (for piping to jq)") .action(list as any) + .command("list", "list all triggers") + .option("--json", "Output as JSON (for piping to jq)") + .action(list as any) + .command("get", "get a trigger's details") + .arguments("") + .option("--json", "Output as JSON (for piping to jq)") + .option("--kind ", "Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email). Recommended for faster lookup") + .action(get as any) + .command("new", "create a new trigger locally") + .arguments("") + .option("--kind ", "Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email)") + .action(newTrigger as any) .command( "push", "push a local trigger spec. This overrides any remote versions." diff --git a/cli/src/commands/user/user.ts b/cli/src/commands/user/user.ts index f5d8891c9c..207958ecce 100644 --- a/cli/src/commands/user/user.ts +++ b/cli/src/commands/user/user.ts @@ -11,8 +11,8 @@ import { compareInstanceObjects, InstanceSyncOptions } from "../instance/instanc import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; -import * as log from "@std/log"; -import { stringify as yamlStringify } from "@std/yaml"; +import * as log from "../../core/log.ts"; +import { stringify as yamlStringify } from "yaml"; import { yamlParseFile } from "../../utils/yaml.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { diff --git a/cli/src/commands/variable/variable.ts b/cli/src/commands/variable/variable.ts index 1fc831bf87..21b7a69eba 100644 --- a/cli/src/commands/variable/variable.ts +++ b/cli/src/commands/variable/variable.ts @@ -1,4 +1,5 @@ -import { stat } from "node:fs/promises"; +import { stat, writeFile } from "node:fs/promises"; +import { stringify as yamlStringify } from "yaml"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace, validatePath } from "../../core/context.ts"; @@ -12,13 +13,13 @@ import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; import { colors } from "@cliffy/ansi/colors"; import { Confirm } from "@cliffy/prompt/confirm"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../../core/log.ts"; +import { sep as SEP } from "node:path"; import * as wmill from "../../../gen/services.gen.ts"; import { ListableVariable } from "../../../gen/types.gen.ts"; -async function list(opts: GlobalOptions) { +async function list(opts: GlobalOptions & { json?: boolean }) { const workspace = await resolveWorkspace(opts); await requireLogin(opts); @@ -26,19 +27,64 @@ async function list(opts: GlobalOptions) { workspace: workspace.workspaceId, }); - new Table() - .header(["Path", "Is Secret", "Account", "Value"]) - .padding(2) - .border(true) - .body( - variables.map((x) => [ - x.path, - x.is_secret ? "true" : "false", - x.account ?? "-", - x.value ?? "-", - ]) - ) - .render(); + if (opts.json) { + console.log(JSON.stringify(variables)); + } else { + new Table() + .header(["Path", "Is Secret", "Account", "Value"]) + .padding(2) + .border(true) + .body( + variables.map((x) => [ + x.path, + x.is_secret ? "true" : "false", + x.account ?? "-", + x.value ?? "-", + ]) + ) + .render(); + } +} + +async function newVariable(opts: GlobalOptions, path: string) { + if (!validatePath(path)) { + return; + } + const filePath = path + ".variable.yaml"; + try { + await stat(filePath); + throw new Error("File already exists: " + filePath); + } catch (e: any) { + if (e.message?.startsWith("File already exists")) throw e; + } + const template: VariableFile = { + value: "", + is_secret: false, + description: "", + }; + await writeFile(filePath, yamlStringify(template as Record), { + flag: "wx", + encoding: "utf-8", + }); + log.info(colors.green(`Created ${filePath}`)); +} + +async function get(opts: GlobalOptions & { json?: boolean }, path: string) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + const v = await wmill.getVariable({ + workspace: workspace.workspaceId, + path, + }); + if (opts.json) { + console.log(JSON.stringify(v)); + } else { + console.log(colors.bold("Path:") + " " + v.path); + console.log(colors.bold("Value:") + " " + (v.value ?? "-")); + console.log(colors.bold("Is Secret:") + " " + (v.is_secret ? "true" : "false")); + console.log(colors.bold("Description:") + " " + (v.description ?? "")); + console.log(colors.bold("Account:") + " " + (v.account ?? "-")); + } } export interface VariableFile { @@ -178,7 +224,18 @@ async function add( const command = new Command() .description("variable related commands") + .option("--json", "Output as JSON (for piping to jq)") .action(list as any) + .command("list", "list all variables") + .option("--json", "Output as JSON (for piping to jq)") + .action(list as any) + .command("get", "get a variable's details") + .arguments("") + .option("--json", "Output as JSON (for piping to jq)") + .action(get as any) + .command("new", "create a new variable locally") + .arguments("") + .action(newVariable as any) .command( "push", "Push a local variable spec. This overrides any remote versions." diff --git a/cli/src/commands/worker-groups/worker-groups.ts b/cli/src/commands/worker-groups/worker-groups.ts index a49769b706..30c14249ad 100644 --- a/cli/src/commands/worker-groups/worker-groups.ts +++ b/cli/src/commands/worker-groups/worker-groups.ts @@ -1,7 +1,7 @@ import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; import { Confirm } from "@cliffy/prompt/confirm"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import { setClient } from "../../core/client.ts"; import { allInstances, getActiveInstance, InstanceSyncOptions, pickInstance } from "../instance/instance.ts"; import * as wmill from "../../../gen/services.gen.ts"; diff --git a/cli/src/commands/workers/workers.ts b/cli/src/commands/workers/workers.ts index 70decb109a..7ea5d074ab 100644 --- a/cli/src/commands/workers/workers.ts +++ b/cli/src/commands/workers/workers.ts @@ -1,6 +1,6 @@ import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { pickInstance } from "../instance/instance.ts"; diff --git a/cli/src/commands/workspace/fork.ts b/cli/src/commands/workspace/fork.ts index 19091d6b95..619f29fa2c 100644 --- a/cli/src/commands/workspace/fork.ts +++ b/cli/src/commands/workspace/fork.ts @@ -1,7 +1,7 @@ import { GlobalOptions } from "../../types.ts"; import { colors } from "@cliffy/ansi/colors"; import { Input } from "@cliffy/prompt/input"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import { setClient } from "../../core/client.ts"; import { allWorkspaces, list, removeWorkspace } from "./workspace.ts"; import * as wmill from "../../../gen/services.gen.ts"; diff --git a/cli/src/commands/workspace/workspace.ts b/cli/src/commands/workspace/workspace.ts index b7464e7cfd..b70469073c 100644 --- a/cli/src/commands/workspace/workspace.ts +++ b/cli/src/commands/workspace/workspace.ts @@ -11,7 +11,7 @@ import { Command } from "@cliffy/command"; import { Confirm } from "@cliffy/prompt/confirm"; import { Input } from "@cliffy/prompt/input"; import { Table } from "@cliffy/table"; -import * as log from "@std/log"; +import * as log from "../../core/log.ts"; import { setClient } from "../../core/client.ts"; import { requireLogin } from "../../core/auth.ts"; import { createWorkspaceFork, deleteWorkspaceFork } from "./fork.ts"; @@ -518,7 +518,7 @@ async function bind( } // Write back the updated config - const { stringify: yamlStringify } = await import("@std/yaml"); + const { stringify: yamlStringify } = await import("yaml"); try { await writeFile("wmill.yaml", yamlStringify(config), "utf-8"); } catch (error) { diff --git a/cli/src/core/auth.ts b/cli/src/core/auth.ts index 311fe16ea7..0be7571320 100644 --- a/cli/src/core/auth.ts +++ b/cli/src/core/auth.ts @@ -1,5 +1,5 @@ import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; +import * as log from "./log.ts"; import { setClient } from "./client.ts"; import * as wmill from "../../gen/services.gen.ts"; import { GlobalUserInfo } from "../../gen/types.gen.ts"; diff --git a/cli/src/core/branch-profiles.ts b/cli/src/core/branch-profiles.ts index 8b1c52f9d5..80c72b0705 100644 --- a/cli/src/core/branch-profiles.ts +++ b/cli/src/core/branch-profiles.ts @@ -1,4 +1,4 @@ -import * as log from "@std/log"; +import * as log from "./log.ts"; import { readFile, writeFile } from "node:fs/promises"; import { getStore } from "./store.ts"; diff --git a/cli/src/core/conf.ts b/cli/src/core/conf.ts index 775f71befb..22acd536f0 100644 --- a/cli/src/core/conf.ts +++ b/cli/src/core/conf.ts @@ -1,7 +1,7 @@ -import * as log from "@std/log"; +import * as log from "./log.ts"; import { yamlParseFile } from "../utils/yaml.ts"; import { Confirm } from "@cliffy/prompt/confirm"; -import { stringify as yamlStringify } from "@std/yaml"; +import { stringify as yamlStringify } from "yaml"; import { getCurrentGitBranch, getOriginalBranchForWorkspaceForks, @@ -196,7 +196,7 @@ export async function readConfigFile(): Promise { if (!wmillYamlPath) { log.warn( - "No wmill.yaml found. Use 'wmill init' to bootstrap it. Using 'bun' as default typescript runtime." + "No wmill.yaml found. Use 'wmill init' to bootstrap it." ); return {}; } diff --git a/cli/src/core/context.ts b/cli/src/core/context.ts index 0e71628599..86307646bf 100644 --- a/cli/src/core/context.ts +++ b/cli/src/core/context.ts @@ -1,5 +1,5 @@ import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; +import * as log from "./log.ts"; import { Select } from "@cliffy/prompt/select"; import { Confirm } from "@cliffy/prompt/confirm"; import { Input } from "@cliffy/prompt/input"; @@ -459,11 +459,12 @@ export async function resolveWorkspace( // forked workspace, that we detect through the branch name (only when not using branchOverride) const res = await tryResolveWorkspace(opts); if (!res.isError) { + const workspace = (res as { isError: false; value: Workspace }).value; if (branchOverride || !branch || !branch.startsWith(WM_FORK_PREFIX)) { - return res.value; + return workspace; } else { log.info( - `Found an active workspace \`${res.value.name}\` but the branch name indicates this is a forked workspace. Ignoring active workspace and trying to resolve the correct workspace from the branch name \`${branch}\`` + `Found an active workspace \`${workspace.name}\` but the branch name indicates this is a forked workspace. Ignoring active workspace and trying to resolve the correct workspace from the branch name \`${branch}\`` ); } } @@ -486,13 +487,41 @@ export async function resolveWorkspace( } } - // Fall back to active workspace (lowest priority) + // Fall back to active workspace const activeWorkspace = await getActiveWorkspace(opts); if (activeWorkspace) { (opts as any).__secret_workspace = activeWorkspace; return activeWorkspace; } + // Last resort: auto-configure from Windmill environment variables + // (set by the worker for bash/script execution) + const envWorkspace = process.env["WM_WORKSPACE"]; + const envToken = process.env["WM_TOKEN"]; + const envBaseUrl = + process.env["BASE_INTERNAL_URL"] ?? process.env["BASE_URL"]; + + if (envWorkspace && envToken && envBaseUrl) { + let normalizedBaseUrl: string; + try { + normalizedBaseUrl = new URL(envBaseUrl).toString(); + } catch { + log.info(colors.red(`Invalid BASE_INTERNAL_URL: ${envBaseUrl}`)); + return process.exit(-1); + } + log.debug( + `Using workspace from environment variables: ${envWorkspace} on ${normalizedBaseUrl}` + ); + const ws: Workspace = { + name: envWorkspace, + workspaceId: envWorkspace, + remote: normalizedBaseUrl, + token: envToken, + }; + (opts as any).__secret_workspace = ws; + return ws; + } + // If everything failed, show error log.info(colors.red.bold("No workspace given and no default set.")); return process.exit(-1); @@ -532,7 +561,8 @@ export async function tryResolveVersion( const workspaceRes = await tryResolveWorkspace(opts); if (workspaceRes.isError) return undefined; - const version = await fetchVersion(workspaceRes.value.remote); + const workspace = (workspaceRes as { isError: false; value: Workspace }).value; + const version = await fetchVersion(workspace.remote); try { return Number.parseInt( diff --git a/cli/src/core/log.ts b/cli/src/core/log.ts new file mode 100644 index 0000000000..d7bed9a0d4 --- /dev/null +++ b/cli/src/core/log.ts @@ -0,0 +1,24 @@ +let logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" = "INFO"; + +const levels = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 }; + +export function setup(level: "DEBUG" | "INFO" | "WARN" | "ERROR") { + logLevel = level; +} + +export function debug(msg: unknown) { + if (levels[logLevel] <= levels.DEBUG) + console.log(`\x1b[90m${String(msg)}\x1b[39m`); +} + +export function info(msg: unknown) { + console.log(`\x1b[34m${String(msg)}\x1b[39m`); +} + +export function warn(msg: unknown) { + console.log(`\x1b[33m${String(msg)}\x1b[39m`); +} + +export function error(msg: unknown) { + console.log(`\x1b[31m${String(msg)}\x1b[39m`); +} diff --git a/cli/src/core/login.ts b/cli/src/core/login.ts index c492347c0f..516b3fb4c8 100644 --- a/cli/src/core/login.ts +++ b/cli/src/core/login.ts @@ -1,7 +1,7 @@ import { GlobalOptions } from "../types.ts"; import { colors } from "@cliffy/ansi/colors"; import * as getPort from "get-port"; -import * as log from "@std/log"; +import * as log from "./log.ts"; import * as open from "open"; import { Secret } from "@cliffy/prompt/secret"; import { Select } from "@cliffy/prompt/select"; diff --git a/cli/src/core/settings.ts b/cli/src/core/settings.ts index 7075ba2edc..3ffddc4837 100644 --- a/cli/src/core/settings.ts +++ b/cli/src/core/settings.ts @@ -2,9 +2,9 @@ import process from "node:process"; import { writeFile } from "node:fs/promises"; import { colors } from "@cliffy/ansi/colors"; import { Confirm } from "@cliffy/prompt/confirm"; -import * as log from "@std/log"; +import * as log from "./log.ts"; import { yamlParseFile } from "../utils/yaml.ts"; -import { stringify as yamlStringify } from "@std/yaml"; +import { stringify as yamlStringify } from "yaml"; import * as wmill from "../../gen/services.gen.ts"; import { AIConfig, Config, GlobalSetting } from "../../gen/types.gen.ts"; import { compareInstanceObjects, InstanceSyncOptions } from "../commands/instance/instance.ts"; diff --git a/cli/src/guidance/skills.ts b/cli/src/guidance/skills.ts index 19f455fadb..88f8cbb842 100644 --- a/cli/src/guidance/skills.ts +++ b/cli/src/guidance/skills.ts @@ -4557,9 +4557,16 @@ Current version: 1.624.0 app related commands +**Options:** +- \`--json\` - Output as JSON (for piping to jq) + **Subcommands:** -- \`app push \` - push a local app +- \`app list\` - list all apps + - \`--json\` - Output as JSON (for piping to jq) +- \`app get \` - get an app's details + - \`--json\` - Output as JSON (for piping to jq) +- \`app push \` - push a local app - \`app dev [app_folder:string]\` - Start a development server for building apps with live reload and hot module replacement - \`--port \` - Port to run the dev server on (will find next available port if occupied) - \`--host \` - Host to bind the dev server to @@ -4596,10 +4603,16 @@ Launch a dev server that will spawn a webserver with HMR flow related commands **Options:** -- \`--show-archived\` - Enable archived scripts in output +- \`--show-archived\` - Enable archived flows in output +- \`--json\` - Output as JSON (for piping to jq) **Subcommands:** +- \`flow list\` - list all flows + - \`--show-archived\` - Enable archived flows in output + - \`--json\` - Output as JSON (for piping to jq) +- \`flow get \` - get a flow's details + - \`--json\` - Output as JSON (for piping to jq) - \`flow push \` - push a local flow spec. This overrides any remote versions. - \`flow run \` - run a flow by path. - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. @@ -4611,16 +4624,27 @@ flow related commands - \`--yes\` - Skip confirmation prompt - \`-i --includes \` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string) - \`-e --excludes \` - Comma separated patterns to specify which file to NOT take into account. -- \`flow bootstrap \` - create a new empty flow - - \`--summary \` - script summary - - \`--description \` - script description +- \`flow new \` - create a new empty flow + - \`--summary \` - flow summary + - \`--description \` - flow description +- \`flow bootstrap \` - create a new empty flow (alias for new) + - \`--summary \` - flow summary + - \`--description \` - flow description ### folder folder related commands +**Options:** +- \`--json\` - Output as JSON (for piping to jq) + **Subcommands:** +- \`folder list\` - list all folders + - \`--json\` - Output as JSON (for piping to jq) +- \`folder get \` - get a folder's details + - \`--json\` - Output as JSON (for piping to jq) +- \`folder new \` - create a new folder locally - \`folder push \` - push a local folder spec. This overrides any remote versions. ### gitsync-settings @@ -4731,18 +4755,33 @@ List all queues with their metrics resource related commands +**Options:** +- \`--json\` - Output as JSON (for piping to jq) + **Subcommands:** +- \`resource list\` - list all resources + - \`--json\` - Output as JSON (for piping to jq) +- \`resource get \` - get a resource's details + - \`--json\` - Output as JSON (for piping to jq) +- \`resource new \` - create a new resource locally - \`resource push \` - push a local resource spec. This overrides any remote versions. ### resource-type resource type related commands +**Options:** +- \`--json\` - Output as JSON (for piping to jq) + **Subcommands:** - \`resource-type list\` - list all resource types - \`--schema\` - Show schema in the output + - \`--json\` - Output as JSON (for piping to jq) +- \`resource-type get \` - get a resource type's details + - \`--json\` - Output as JSON (for piping to jq) +- \`resource-type new \` - create a new resource type locally - \`resource-type push \` - push a local resource spec. This overrides any remote versions. - \`resource-type generate-namespace\` - Create a TypeScript definition file with the RT namespace generated from the resource types @@ -4750,8 +4789,16 @@ resource type related commands schedule related commands +**Options:** +- \`--json\` - Output as JSON (for piping to jq) + **Subcommands:** +- \`schedule list\` - list all schedules + - \`--json\` - Output as JSON (for piping to jq) +- \`schedule get \` - get a schedule's details + - \`--json\` - Output as JSON (for piping to jq) +- \`schedule new \` - create a new schedule locally - \`schedule push \` - push a local schedule spec. This overrides any remote versions. ### script @@ -4760,21 +4807,30 @@ script related commands **Options:** - \`--show-archived\` - Enable archived scripts in output +- \`--json\` - Output as JSON (for piping to jq) **Subcommands:** -- \`script push \` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh -- \`script show \` - show a scripts content +- \`script list\` - list all scripts + - \`--show-archived\` - Enable archived scripts in output + - \`--json\` - Output as JSON (for piping to jq) +- \`script get \` - get a script's details + - \`--json\` - Output as JSON (for piping to jq) +- \`script show \` - show a script's content (alias for get) +- \`script push \` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh) - \`script run \` - run a script by path - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. - \`-s --silent\` - Do not output anything other then the final output. Useful for scripting. - \`script preview \` - preview a local script without deploying it. Supports both regular and codebase scripts. - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. - \`-s --silent\` - Do not output anything other than the final output. Useful for scripting. -- \`script bootstrap \` - create a new script +- \`script new \` - create a new script - \`--summary \` - script summary - \`--description \` - script description -- \`script generate-metadata [script:file]\` - re-generate the metadata file updating the lock and the script schema (for flows, use \`wmill flow generate-locks\` +- \`script bootstrap \` - create a new script (alias for new) + - \`--summary \` - script summary + - \`--description \` - script description +- \`script generate-metadata [script:file]\` - re-generate the metadata file updating the lock and the script schema (for flows, use \`wmill flow generate-locks\`) - \`--yes\` - Skip confirmation prompt - \`--dry-run\` - Perform a dry run without making changes - \`--lock-only\` - re-generate only the lock @@ -4852,8 +4908,18 @@ sync local with a remote workspaces or the opposite (push or pull) trigger related commands +**Options:** +- \`--json\` - Output as JSON (for piping to jq) + **Subcommands:** +- \`trigger list\` - list all triggers + - \`--json\` - Output as JSON (for piping to jq) +- \`trigger get \` - get a trigger's details + - \`--json\` - Output as JSON (for piping to jq) + - \`--kind \` - Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email) +- \`trigger new \` - create a new trigger locally + - \`--kind \` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email) - \`trigger push \` - push a local trigger spec. This overrides any remote versions. ### user @@ -4875,8 +4941,16 @@ user related commands variable related commands +**Options:** +- \`--json\` - Output as JSON (for piping to jq) + **Subcommands:** +- \`variable list\` - list all variables + - \`--json\` - Output as JSON (for piping to jq) +- \`variable get \` - get a variable's details + - \`--json\` - Output as JSON (for piping to jq) +- \`variable new \` - create a new variable locally - \`variable push \` - Push a local variable spec. This overrides any remote versions. - \`--plain-secrets\` - Push secrets as plain text - \`variable add \` - Create a new variable on the remote. This will update the variable if it already exists. diff --git a/cli/src/main.ts b/cli/src/main.ts index e7ace077c6..cfd366081a 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -1,7 +1,7 @@ import { Command } from "@cliffy/command"; import { CompletionsCommand } from "@cliffy/command/completions"; import { UpgradeCommand } from "@cliffy/command/upgrade"; -import * as log from "@std/log"; +import * as log from "./core/log.ts"; import { realpathSync } from "node:fs"; import { fileURLToPath } from "node:url"; @@ -28,7 +28,7 @@ import lint from "./commands/lint/lint.ts"; import dev from "./commands/dev/dev.ts"; import { GlobalOptions } from "./types.ts"; import { OpenAPI } from "../gen/index.ts"; -import { getHeaders, getIsWin } from "./utils/utils.ts"; +import { getHeaders } from "./utils/utils.ts"; import { setShowDiffs } from "./core/conf.ts"; import { NpmProvider } from "./utils/upgrade.ts"; import { pull as hubPull } from "./commands/hub/hub.ts"; @@ -187,21 +187,7 @@ async function main() { // const NO_COLORS = args.includes("--no-colors"); setShowDiffs(args.includes("--show-diffs")); - const isWin = await getIsWin(); - log.setup({ - handlers: { - console: new log.ConsoleHandler(LOG_LEVEL, { - formatter: ({ msg }) => msg, - useColors: isWin ? false : true, - }), - }, - loggers: { - default: { - level: LOG_LEVEL, - handlers: ["console"], - }, - }, - }); + log.setup(LOG_LEVEL); log.debug("Debug logging enabled. CLI build against " + VERSION); const extraHeaders = getHeaders(); @@ -235,7 +221,10 @@ function isMain() { } } if (isMain()) { - main(); + main().then(() => { + // Destroy stdin so interactive prompts (Cliffy) don't keep the event loop alive + process.stdin.destroy(); + }); } export default command; diff --git a/cli/src/types.ts b/cli/src/types.ts index 7115f743ff..382f7f82af 100644 --- a/cli/src/types.ts +++ b/cli/src/types.ts @@ -1,9 +1,9 @@ import { colors } from "@cliffy/ansi/colors"; import * as Diff from "diff"; -import * as log from "@std/log"; -import * as path from "@std/path"; -import { SEPARATOR as SEP } from "@std/path"; -import { stringify as yamlStringify } from "@std/yaml"; +import * as log from "./core/log.ts"; +import * as path from "node:path"; +import { sep as SEP } from "node:path"; +import { stringify as yamlStringify } from "yaml"; import { yamlParseContent } from "./utils/yaml.ts"; import { readFileSync } from "node:fs"; import { pushApp } from "./commands/app/app.ts"; diff --git a/cli/src/utils/codebase.ts b/cli/src/utils/codebase.ts index 2fdad891d6..e665d060b2 100644 --- a/cli/src/utils/codebase.ts +++ b/cli/src/utils/codebase.ts @@ -1,5 +1,5 @@ import { Codebase, SyncOptions } from "../core/conf.ts"; -import * as log from "@std/log"; +import * as log from "../core/log.ts"; import { digestDir } from "./utils.ts"; export type SyncCodebase = Codebase & { diff --git a/cli/src/utils/git.ts b/cli/src/utils/git.ts index c402a5906a..05e37240dd 100644 --- a/cli/src/utils/git.ts +++ b/cli/src/utils/git.ts @@ -1,4 +1,4 @@ -import * as log from "@std/log"; +import * as log from "../core/log.ts"; import { execSync } from "node:child_process"; import { WM_FORK_PREFIX } from "../core/constants.ts"; diff --git a/cli/src/utils/metadata.ts b/cli/src/utils/metadata.ts index 7ddba177b3..0e10798725 100644 --- a/cli/src/utils/metadata.ts +++ b/cli/src/utils/metadata.ts @@ -1,8 +1,8 @@ import { GlobalOptions } from "../types.ts"; -import { SEPARATOR as SEP } from "@std/path"; +import { sep as SEP } from "node:path"; import { colors } from "@cliffy/ansi/colors"; -import * as log from "@std/log"; -import { stringify as yamlStringify } from "@std/yaml"; +import * as log from "../core/log.ts"; +import { stringify as yamlStringify } from "yaml"; import { yamlParseFile } from "./yaml.ts"; import { readFile, writeFile, stat, rm, readdir } from "node:fs/promises"; import { readFileSync } from "node:fs"; diff --git a/cli/src/utils/resource_folders.ts b/cli/src/utils/resource_folders.ts index 898edb305c..e24835ab35 100644 --- a/cli/src/utils/resource_folders.ts +++ b/cli/src/utils/resource_folders.ts @@ -8,8 +8,8 @@ * (.flow, .app, .raw_app) or dunder-prefixed names (__flow, __app, __raw_app). */ -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../core/log.ts"; +import { sep as SEP } from "node:path"; import { yamlParseFile } from "./yaml.ts"; import * as fs from "node:fs"; import * as path from "node:path"; diff --git a/cli/src/utils/tar.ts b/cli/src/utils/tar.ts new file mode 100644 index 0000000000..d5149acb56 --- /dev/null +++ b/cli/src/utils/tar.ts @@ -0,0 +1,22 @@ +import { pack } from "tar-stream"; + +export interface TarEntry { + name: string; + content: Buffer | Uint8Array | string; +} + +export function createTarBlob(entries: TarEntry[]): Promise { + return new Promise((resolve, reject) => { + const p = pack(); + const chunks: Uint8Array[] = []; + + p.on("data", (chunk: Buffer) => chunks.push(new Uint8Array(chunk))); + p.on("end", () => resolve(new Blob(chunks as BlobPart[]))); + p.on("error", reject); + + for (const entry of entries) { + p.entry({ name: entry.name }, Buffer.from(entry.content)); + } + p.finalize(); + }); +} diff --git a/cli/src/utils/upgrade.ts b/cli/src/utils/upgrade.ts index 709d59eb7f..44ab0741fd 100644 --- a/cli/src/utils/upgrade.ts +++ b/cli/src/utils/upgrade.ts @@ -53,6 +53,10 @@ export class NpmProvider extends Provider { getRegistryUrl(name: string, version: string): string { return `npm:${this.packageName ?? name}@${version}`; } + + async hasRequiredPermissions(): Promise { + return true; + } } type NpmApiPackageMetadata = { diff --git a/cli/src/utils/utils.ts b/cli/src/utils/utils.ts index 8448ef9b9a..02a8c1a643 100644 --- a/cli/src/utils/utils.ts +++ b/cli/src/utils/utils.ts @@ -3,9 +3,8 @@ // @ts-nocheck This file is copied from a JS project, so it's not type-safe. import { colors } from "@cliffy/ansi/colors"; -import { encodeHex } from "@std/encoding"; -import * as log from "@std/log"; -import { SEPARATOR as SEP } from "@std/path"; +import * as log from "../core/log.ts"; +import { sep as SEP } from "node:path"; import crypto from "node:crypto"; import { readFileSync, writeFileSync } from "node:fs"; import { readdir, readFile } from "node:fs/promises"; @@ -128,7 +127,7 @@ export async function generateHashFromBuffer( content: BufferSource ): Promise { const hashBuffer = await crypto.subtle.digest("SHA-256", content); - return encodeHex(hashBuffer); + return Buffer.from(hashBuffer).toString("hex"); } export function readInlinePathSync(path: string): string { diff --git a/cli/src/utils/yaml.ts b/cli/src/utils/yaml.ts index f8621ba2b6..9ad247c1fd 100644 --- a/cli/src/utils/yaml.ts +++ b/cli/src/utils/yaml.ts @@ -1,4 +1,4 @@ -import { parse as yamlParse, type ParseOptions } from "@std/yaml"; +import { parse as yamlParse, type ParseOptions } from "yaml"; import { readFile } from "node:fs/promises"; export async function yamlParseFile(path: string, options: ParseOptions = {}) { diff --git a/cli/test/lint_command.test.ts b/cli/test/lint_command.test.ts index 6b3d91244e..d0f4226568 100644 --- a/cli/test/lint_command.test.ts +++ b/cli/test/lint_command.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "bun:test"; import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises"; import os from "node:os"; -import * as path from "@std/path"; +import * as path from "node:path"; import { formatValidationError, runLint, diff --git a/cli/test/lint_locks.test.ts b/cli/test/lint_locks.test.ts index 742e2318df..6ec4363e82 100644 --- a/cli/test/lint_locks.test.ts +++ b/cli/test/lint_locks.test.ts @@ -1,7 +1,7 @@ import { expect, test, describe } from "bun:test"; import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises"; import os from "node:os"; -import * as path from "@std/path"; +import * as path from "node:path"; import { checkMissingLocks, runLint } from "../src/commands/lint/lint.ts"; async function withTempDir( diff --git a/cli/test/list_get_new_commands.test.ts b/cli/test/list_get_new_commands.test.ts new file mode 100644 index 0000000000..8df67cab9b --- /dev/null +++ b/cli/test/list_get_new_commands.test.ts @@ -0,0 +1,639 @@ +/** + * Integration tests for the new list/get/new CLI commands. + * + * Tests: + * - `list --json` for all item types + * - `get ` and `get --json` for all item types + * - `new` (bootstrap) for script, flow, resource, resource-type, variable, schedule, folder, trigger + * - `bootstrap` alias for script and flow + */ + +import { expect, test, describe } from "bun:test"; +import { writeFile, mkdir, stat, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { withTestBackend, type TestBackend } from "./test_backend.ts"; +import { addWorkspace } from "../workspace.ts"; + +async function setupWorkspaceProfile(backend: TestBackend): Promise { + await addWorkspace( + { + remote: backend.baseUrl, + workspaceId: backend.workspace, + name: "localhost_test", + token: backend.token!, + }, + { force: true, configDir: backend.testConfigDir } + ); +} + +async function createRemoteScript( + backend: TestBackend, + scriptPath: string, + content: string = 'export async function main() { return "hello"; }' +): Promise { + const resp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: scriptPath, + content, + language: "bun", + summary: "Test script summary", + description: "Test script description", + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + required: [], + }, + }), + } + ); + expect(resp.status).toBeLessThan(300); + await resp.text(); +} + +// ============================================================================= +// list --json +// ============================================================================= + +describe("list --json flag", () => { + test("script list --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const scriptPath = `f/test/list_json_script_${uniqueId}`; + await createRemoteScript(backend, scriptPath); + + const result = await backend.runCLICommand( + ["script", "list", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.some((s: any) => s.path === scriptPath)).toBe(true); + }); + }); + + test("flow list --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["flow", "list", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + }); + }); + + test("resource list --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["resource", "list", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + // seedTestData creates f/test/my_resource + expect(parsed.some((r: any) => r.path === "f/test/my_resource")).toBe( + true + ); + }); + }); + + test("variable list --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["variable", "list", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + }); + }); + + test("folder list --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["folder", "list", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.some((f: any) => f.name === "test")).toBe(true); + }); + }); + + test("schedule list --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["schedule", "list", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + }); + }); + + test("resource-type list --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["resource-type", "list", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + }); + }); + + test("trigger list --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["trigger", "list", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + }); + }); + + test("app list --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["app", "list", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + }); + }); + + test("default action with --json works (e.g. wmill script --json)", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["script", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + }); + }); +}); + +// ============================================================================= +// get and get --json +// ============================================================================= + +describe("get command", () => { + test("script get pretty-prints details", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const scriptPath = `f/test/get_script_${uniqueId}`; + await createRemoteScript(backend, scriptPath); + + const result = await backend.runCLICommand( + ["script", "get", scriptPath], + tempDir + ); + + expect(result.code).toEqual(0); + const output = result.stdout; + expect(output).toContain("Path:"); + expect(output).toContain(scriptPath); + expect(output).toContain("Summary:"); + expect(output).toContain("Language:"); + expect(output).toContain("bun"); + }); + }); + + test("script get --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const scriptPath = `f/test/get_json_script_${uniqueId}`; + await createRemoteScript(backend, scriptPath); + + const result = await backend.runCLICommand( + ["script", "get", scriptPath, "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(parsed.path).toBe(scriptPath); + expect(parsed.language).toBe("bun"); + expect(parsed.summary).toBe("Test script summary"); + }); + }); + + test("resource get --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["resource", "get", "f/test/my_resource", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(parsed.path).toBe("f/test/my_resource"); + expect(parsed.resource_type).toBe("any"); + }); + }); + + test("resource get pretty-prints details", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["resource", "get", "f/test/my_resource"], + tempDir + ); + + expect(result.code).toEqual(0); + const output = result.stdout; + expect(output).toContain("Path:"); + expect(output).toContain("f/test/my_resource"); + expect(output).toContain("Resource Type:"); + }); + }); + + test("variable get --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["variable", "get", "f/test/my_variable", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(parsed.path).toBe("f/test/my_variable"); + }); + }); + + test("folder get --json outputs valid JSON", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["folder", "get", "test", "--json"], + tempDir + ); + + expect(result.code).toEqual(0); + const parsed = JSON.parse(result.stdout); + expect(parsed.name).toBe("test"); + }); + }); + + test("folder get pretty-prints details", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["folder", "get", "test"], + tempDir + ); + + expect(result.code).toEqual(0); + const output = result.stdout; + expect(output).toContain("Name:"); + expect(output).toContain("test"); + }); + }); +}); + +// ============================================================================= +// new command +// ============================================================================= + +describe("new command", () => { + test("script new creates files (same as bootstrap)", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["script", "new", "f/test/new_cmd_script", "bun", "--summary", "Test new"], + tempDir + ); + + expect(result.code).toEqual(0); + + const codeStat = await stat(join(tempDir, "f/test/new_cmd_script.ts")); + expect(codeStat.isFile()).toBe(true); + + const metaStat = await stat( + join(tempDir, "f/test/new_cmd_script.script.yaml") + ); + expect(metaStat.isFile()).toBe(true); + + const metaContent = await readFile( + join(tempDir, "f/test/new_cmd_script.script.yaml"), + "utf-8" + ); + expect(metaContent).toContain("Test new"); + }); + }); + + test("script bootstrap still works as alias", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await writeFile( + join(tempDir, "wmill.yaml"), + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8" + ); + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["script", "bootstrap", "f/test/alias_script", "bun"], + tempDir + ); + + expect(result.code).toEqual(0); + + const codeStat = await stat(join(tempDir, "f/test/alias_script.ts")); + expect(codeStat.isFile()).toBe(true); + }); + }); + + test("flow new creates flow directory and flow.yaml", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["flow", "new", "f/test/new_flow", "--summary", "My flow"], + tempDir + ); + + expect(result.code).toEqual(0); + + const flowYamlStat = await stat( + join(tempDir, "f/test/new_flow.flow/flow.yaml") + ); + expect(flowYamlStat.isFile()).toBe(true); + + const flowContent = await readFile( + join(tempDir, "f/test/new_flow.flow/flow.yaml"), + "utf-8" + ); + expect(flowContent).toContain("My flow"); + }); + }); + + test("flow bootstrap still works as alias", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["flow", "bootstrap", "f/test/alias_flow"], + tempDir + ); + + expect(result.code).toEqual(0); + + const flowYamlStat = await stat( + join(tempDir, "f/test/alias_flow.flow/flow.yaml") + ); + expect(flowYamlStat.isFile()).toBe(true); + }); + }); + + test("resource new creates resource yaml template", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["resource", "new", "f/test/new_resource"], + tempDir + ); + + expect(result.code).toEqual(0); + + const filePath = join(tempDir, "f/test/new_resource.resource.yaml"); + const fileStat = await stat(filePath); + expect(fileStat.isFile()).toBe(true); + + const content = await readFile(filePath, "utf-8"); + expect(content).toContain("resource_type"); + expect(content).toContain("value"); + }); + }); + + test("resource-type new creates resource-type yaml template", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["resource-type", "new", "my_custom_type"], + tempDir + ); + + expect(result.code).toEqual(0); + + const filePath = join(tempDir, "my_custom_type.resource-type.yaml"); + const fileStat = await stat(filePath); + expect(fileStat.isFile()).toBe(true); + + const content = await readFile(filePath, "utf-8"); + expect(content).toContain("schema"); + expect(content).toContain("description"); + }); + }); + + test("variable new creates variable yaml template", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["variable", "new", "f/test/new_var"], + tempDir + ); + + expect(result.code).toEqual(0); + + const filePath = join(tempDir, "f/test/new_var.variable.yaml"); + const fileStat = await stat(filePath); + expect(fileStat.isFile()).toBe(true); + + const content = await readFile(filePath, "utf-8"); + expect(content).toContain("is_secret"); + expect(content).toContain("value"); + }); + }); + + test("schedule new creates schedule yaml template", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["schedule", "new", "f/test/new_sched"], + tempDir + ); + + expect(result.code).toEqual(0); + + const filePath = join(tempDir, "f/test/new_sched.schedule.yaml"); + const fileStat = await stat(filePath); + expect(fileStat.isFile()).toBe(true); + + const content = await readFile(filePath, "utf-8"); + expect(content).toContain("schedule"); + expect(content).toContain("script_path"); + expect(content).toContain("timezone"); + }); + }); + + test("folder new creates folder.meta.yaml in f//", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const result = await backend.runCLICommand( + ["folder", "new", "new_folder"], + tempDir + ); + + expect(result.code).toEqual(0); + + const filePath = join(tempDir, "f/new_folder/folder.meta.yaml"); + const fileStat = await stat(filePath); + expect(fileStat.isFile()).toBe(true); + + const content = await readFile(filePath, "utf-8"); + expect(content).toContain("owners"); + expect(content).toContain("extra_perms"); + }); + }); + + test("trigger new --kind http creates http trigger yaml template", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["trigger", "new", "f/test/new_trigger", "--kind", "http"], + tempDir + ); + + expect(result.code).toEqual(0); + + const filePath = join( + tempDir, + "f/test/new_trigger.http_trigger.yaml" + ); + const fileStat = await stat(filePath); + expect(fileStat.isFile()).toBe(true); + + const content = await readFile(filePath, "utf-8"); + expect(content).toContain("script_path"); + expect(content).toContain("route_path"); + }); + }); + + test("trigger new without --kind fails with error", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["trigger", "new", "f/test/fail_trigger"], + tempDir + ); + + expect(result.code).not.toEqual(0); + }); + }); + + test("trigger new --kind kafka creates kafka trigger yaml template", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + await mkdir(join(tempDir, "f", "test"), { recursive: true }); + + const result = await backend.runCLICommand( + ["trigger", "new", "f/test/kafka_trigger", "--kind", "kafka"], + tempDir + ); + + expect(result.code).toEqual(0); + + const filePath = join( + tempDir, + "f/test/kafka_trigger.kafka_trigger.yaml" + ); + const fileStat = await stat(filePath); + expect(fileStat.isFile()).toBe(true); + + const content = await readFile(filePath, "utf-8"); + expect(content).toContain("kafka_resource_path"); + expect(content).toContain("topics"); + }); + }); +}); diff --git a/cli/test/lock_cache.test.ts b/cli/test/lock_cache.test.ts index db1632bb27..9f30b67d90 100644 --- a/cli/test/lock_cache.test.ts +++ b/cli/test/lock_cache.test.ts @@ -11,7 +11,7 @@ */ import { expect, test } from "bun:test"; -import { encodeHex } from "@std/encoding"; +import { Buffer } from "node:buffer"; // --------------------------------------------------------------------------- // Mirrors extractWorkspaceDepsAnnotation + computeLockCacheKey from @@ -110,7 +110,7 @@ async function computeLockCacheKey( .join(";"); const content = `${language}|${annotationStr}|${depsStr}`; const buf = new TextEncoder().encode(content); - return encodeHex(await crypto.subtle.digest("SHA-256", buf)); + return Buffer.from(await crypto.subtle.digest("SHA-256", buf)).toString("hex"); } // --------------------------------------------------------------------------- diff --git a/cli/test/mixed_case_paths.test.ts b/cli/test/mixed_case_paths.test.ts index d0518cb511..ffbb3f21d4 100644 --- a/cli/test/mixed_case_paths.test.ts +++ b/cli/test/mixed_case_paths.test.ts @@ -13,7 +13,7 @@ */ import { expect, test } from "bun:test"; -import * as path from "@std/path"; +import * as path from "node:path"; import { writeFile, readFile, stat } from "node:fs/promises"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; diff --git a/cli/test/raw_app_sync.test.ts b/cli/test/raw_app_sync.test.ts index e9151b45fc..f77b4b045b 100644 --- a/cli/test/raw_app_sync.test.ts +++ b/cli/test/raw_app_sync.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "bun:test"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; -import * as path from "@std/path"; +import * as path from "node:path"; import { writeFile, readFile, stat, rm, mkdir } from "node:fs/promises"; // ============================================================================= diff --git a/cli/test/sync_pull_push.test.ts b/cli/test/sync_pull_push.test.ts index 7abe61edab..2b062eb157 100644 --- a/cli/test/sync_pull_push.test.ts +++ b/cli/test/sync_pull_push.test.ts @@ -6,8 +6,8 @@ */ import { expect, test, describe } from "bun:test"; -import * as path from "@std/path"; -import { SEPARATOR as SEP } from "@std/path"; +import * as path from "node:path"; +import { sep as SEP } from "node:path"; import { writeFile, readFile, readdir, rm, mkdir, mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; diff --git a/cli/test/tar_creation.test.ts b/cli/test/tar_creation.test.ts new file mode 100644 index 0000000000..accbbb3829 --- /dev/null +++ b/cli/test/tar_creation.test.ts @@ -0,0 +1,140 @@ +/** + * Unit tests for the tar creation utility. + * These tests require no backend — they test standalone tar logic. + */ + +import { expect, test, describe } from "bun:test"; +import { createTarBlob, type TarEntry } from "../src/utils/tar.ts"; +import { extract, type Headers } from "tar-stream"; +import { Readable } from "node:stream"; + +/** Extract all entries from a tarball Blob into a map of name -> content string */ +async function extractTar( + blob: Blob +): Promise> { + const result = new Map(); + const ex = extract(); + const buffer = Buffer.from(await blob.arrayBuffer()); + + return new Promise((resolve, reject) => { + ex.on("entry", (header, stream, next) => { + const chunks: Buffer[] = []; + stream.on("data", (chunk: Buffer) => chunks.push(chunk)); + stream.on("end", () => { + result.set(header.name, { + content: Buffer.concat(chunks).toString("utf-8"), + header, + }); + next(); + }); + stream.on("error", reject); + stream.resume(); + }); + ex.on("finish", () => resolve(result)); + ex.on("error", reject); + + Readable.from(buffer).pipe(ex); + }); +} + +describe("createTarBlob", () => { + test("single file tarball", async () => { + const entries: TarEntry[] = [ + { name: "main.js", content: 'console.log("hello");' }, + ]; + + const blob = await createTarBlob(entries); + const extracted = await extractTar(blob); + + expect(extracted.size).toBe(1); + expect(extracted.has("main.js")).toBe(true); + expect(extracted.get("main.js")!.content).toBe('console.log("hello");'); + }); + + test("multiple output files", async () => { + const entries: TarEntry[] = [ + { name: "main.js", content: 'import "./chunk-abc.js";' }, + { name: "chunk-abc.js", content: "export const x = 42;" }, + { name: "chunk-def.js", content: "export const y = 99;" }, + ]; + + const blob = await createTarBlob(entries); + const extracted = await extractTar(blob); + + expect(extracted.size).toBe(3); + expect(extracted.get("main.js")!.content).toBe( + 'import "./chunk-abc.js";' + ); + expect(extracted.get("chunk-abc.js")!.content).toBe( + "export const x = 42;" + ); + expect(extracted.get("chunk-def.js")!.content).toBe( + "export const y = 99;" + ); + }); + + test("single file with assets", async () => { + const entries: TarEntry[] = [ + { name: "main.js", content: "const data = require('./data.json');" }, + { name: "data.json", content: '{"key":"value"}' }, + ]; + + const blob = await createTarBlob(entries); + const extracted = await extractTar(blob); + + expect(extracted.size).toBe(2); + expect(extracted.has("main.js")).toBe(true); + expect(extracted.has("data.json")).toBe(true); + expect(extracted.get("data.json")!.content).toBe('{"key":"value"}'); + }); + + test("produces a valid Blob", async () => { + const entries: TarEntry[] = [ + { name: "main.js", content: "module.exports = {};" }, + ]; + + const blob = await createTarBlob(entries); + + expect(blob).toBeInstanceOf(Blob); + expect(blob.size).toBeGreaterThan(0); + // Tar blocks are 512-byte aligned + expect(blob.size % 512).toBe(0); + }); + + test("file naming — entries have exact names given", async () => { + const entries: TarEntry[] = [ + { name: "main.js", content: "entry point" }, + { name: "lib/utils.js", content: "utils" }, + ]; + + const blob = await createTarBlob(entries); + const extracted = await extractTar(blob); + + // Names should be exactly as provided (no leading slash) + expect(extracted.has("main.js")).toBe(true); + expect(extracted.has("lib/utils.js")).toBe(true); + }); + + test("handles Buffer content", async () => { + const entries: TarEntry[] = [ + { name: "main.js", content: Buffer.from("buffer content") }, + ]; + + const blob = await createTarBlob(entries); + const extracted = await extractTar(blob); + + expect(extracted.get("main.js")!.content).toBe("buffer content"); + }); + + test("handles Uint8Array content", async () => { + const content = new TextEncoder().encode("uint8 content"); + const entries: TarEntry[] = [ + { name: "main.js", content }, + ]; + + const blob = await createTarBlob(entries); + const extracted = await extractTar(blob); + + expect(extracted.get("main.js")!.content).toBe("uint8 content"); + }); +}); diff --git a/cli/test/wmill_lock.test.ts b/cli/test/wmill_lock.test.ts index 7760166fd4..22c6c52ea0 100644 --- a/cli/test/wmill_lock.test.ts +++ b/cli/test/wmill_lock.test.ts @@ -7,7 +7,7 @@ */ import { expect, test } from "bun:test"; -import * as path from "@std/path"; +import * as path from "node:path"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import { @@ -18,7 +18,7 @@ import { clearGlobalLock, } from "../src/utils/metadata.ts"; import { generateHash } from "../src/utils/utils.ts"; -import { stringify as yamlStringify } from "@std/yaml"; +import { stringify as yamlStringify } from "yaml"; import { yamlParseFile } from "../src/utils/yaml.ts"; // ============================================================================= diff --git a/cli/test/workspace_deps_filter.test.ts b/cli/test/workspace_deps_filter.test.ts index 52fa6a7b76..c184e9a524 100644 --- a/cli/test/workspace_deps_filter.test.ts +++ b/cli/test/workspace_deps_filter.test.ts @@ -14,7 +14,7 @@ import { expect, test } from "bun:test"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; import { writeFile, mkdir } from "node:fs/promises"; -import { stringify as stringifyYaml } from "@std/yaml"; +import { stringify as stringifyYaml } from "yaml"; // Import hash generation utilities from CLI import { generateHash } from "../src/utils/utils.ts"; diff --git a/cli/windmill-utils-internal/src/parse/parse-schema.ts b/cli/windmill-utils-internal/src/parse/parse-schema.ts index 1887ef2d40..6e94c0ff77 100644 --- a/cli/windmill-utils-internal/src/parse/parse-schema.ts +++ b/cli/windmill-utils-internal/src/parse/parse-schema.ts @@ -228,7 +228,7 @@ export function argSigToJsonSchemaType( if (oldS.items && typeof oldS.items === "object") { ITEMS_PRESERVED_FIELDS.forEach((field) => { if (oldS.items && oldS.items[field] !== undefined) { - newS.items![field] = oldS.items[field]; + (newS.items as any)[field] = oldS.items[field]; } }); } @@ -241,7 +241,7 @@ export function argSigToJsonSchemaType( if (oldS.items && typeof oldS.items === "object") { ITEMS_PRESERVED_FIELDS.forEach((field) => { if (oldS.items && oldS.items[field] !== undefined) { - newS.items![field] = oldS.items[field]; + (newS.items as any)[field] = oldS.items[field]; } }); } diff --git a/docker/DockerfileCli b/docker/DockerfileCli index a3e5d80cb2..546ce49924 100644 --- a/docker/DockerfileCli +++ b/docker/DockerfileCli @@ -1,5 +1,7 @@ -FROM node:slim +FROM oven/bun:slim -RUN npm install -g windmill-cli +RUN bun install -g windmill-cli -ENTRYPOINT [ "wmill" ] \ No newline at end of file +RUN ln -s $(bun pm bin -g)/wmill /usr/bin/wmill + +ENTRYPOINT [ "wmill" ] diff --git a/docker/DockerfileSlim b/docker/DockerfileSlim index 155746cd64..d20067e862 100644 --- a/docker/DockerfileSlim +++ b/docker/DockerfileSlim @@ -56,6 +56,11 @@ RUN mkdir -p /tmp/windmill/cache && \ COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun +# Install windmill CLI (node symlink needed for bun install) +RUN ln -s /usr/bin/bun /usr/bin/node \ + && bun install -g windmill-cli \ + && ln -s $(bun pm bin -g)/wmill /usr/bin/wmill + # add the docker client to call docker from a worker if enabled COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/ diff --git a/docker/DockerfileSlimEe b/docker/DockerfileSlimEe index 53fc9ae51d..8510ce59ad 100644 --- a/docker/DockerfileSlimEe +++ b/docker/DockerfileSlimEe @@ -56,6 +56,11 @@ RUN mkdir -p /tmp/windmill/cache && \ COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun +# Install windmill CLI (node symlink needed for bun install) +RUN ln -s /usr/bin/bun /usr/bin/node \ + && bun install -g windmill-cli \ + && ln -s $(bun pm bin -g)/wmill /usr/bin/wmill + # add the docker client to call docker from a worker if enabled COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1c6e10c9e9..ca78b11190 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -835,7 +835,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -847,7 +846,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -858,7 +856,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1348,7 +1345,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", "integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1503,7 +1499,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1520,7 +1515,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1537,7 +1531,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1554,7 +1547,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1571,7 +1563,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1588,7 +1579,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1605,7 +1595,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1622,7 +1611,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1639,7 +1627,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1656,7 +1643,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1673,7 +1659,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1690,7 +1675,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1707,7 +1691,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2313,7 +2296,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7193,7 +7175,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7692,7 +7674,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7713,7 +7694,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7734,7 +7714,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7755,7 +7734,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7776,7 +7754,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7797,7 +7774,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7818,7 +7794,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7839,7 +7814,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7860,7 +7834,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7881,7 +7854,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7902,7 +7874,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12529,21 +12500,6 @@ } } }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index 9807ed877c..3dfc25e127 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -543,8 +543,7 @@ } editor.insertAtCursor(`v, _ := wmill.GetVariable("${path}")`) } else if (lang == 'bash') { - editor.insertAtCursor(`curl -s -H "Authorization: Bearer $WM_TOKEN" \\ - "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/variables/get_value/${path}" | jq -r .`) + editor.insertAtCursor(`wmill variable get ${path} --json | jq -r .value`) } else if (lang == 'powershell') { editor.insertAtCursor(`$Headers = @{\n"Authorization" = "Bearer $Env:WM_TOKEN"`) editor.arrowDown() @@ -620,8 +619,7 @@ string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri); } editor.insertAtCursor(`r, _ := wmill.GetResource("${path}")`) } else if (lang == 'bash') { - editor.insertAtCursor(`curl -s -H "Authorization: Bearer $WM_TOKEN" \\ - "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/resources/get_value_interpolated/${path}" | jq`) + editor.insertAtCursor(`wmill resource get ${path} --json | jq .value`) } else if (lang == 'powershell') { editor.insertAtCursor(`$Headers = @{\n"Authorization" = "Bearer $Env:WM_TOKEN"`) editor.arrowDown() From 3c89c28e713d6b38d3c4eeacb54e273f8c01496c Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sun, 22 Feb 2026 09:20:55 +0100 Subject: [PATCH 11/16] chore: fix flaky agent token test by not splitting on underscore (#8048) Base64url encoding uses '_' as a valid character, so splitting the JWT token on '_' would intermittently break the JWT parsing when the encoded payload or signature contained underscores. Strip the known prefix instead. Co-authored-by: Claude Opus 4.6 --- integration_tests/test/agent_workers.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/integration_tests/test/agent_workers.py b/integration_tests/test/agent_workers.py index 4b0c616433..9ced69b5c5 100644 --- a/integration_tests/test/agent_workers.py +++ b/integration_tests/test/agent_workers.py @@ -166,15 +166,14 @@ class TestAgentWorkers(unittest.TestCase): print(f"Agent token tests for token: {token}") self.assertIsNotNone(token) - # JWT tokens have the format: jwt_agent__ - self.assertTrue(token.startswith("jwt_agent_"), "Token should start with jwt_agent_") + # JWT tokens have the format: jwt_agent_ + prefix = "jwt_agent_" + self.assertTrue(token.startswith(prefix), "Token should start with jwt_agent_") - # Test that it's a valid JWT format (should contain 2 dots in the JWT part) - parts = token.split('_') - self.assertGreaterEqual(len(parts), 3, "Token should have at least 3 parts separated by underscores") - - # The actual JWT is after the second underscore - jwt_part = parts[2] + # Extract the JWT by stripping the known prefix (don't split on '_' + # because base64url encoding uses '_' as a valid character) + jwt_part = token[len(prefix):] + self.assertGreater(len(jwt_part), 0, "JWT part should not be empty") self.assertEqual(jwt_part.count('.'), 2, "JWT should contain exactly 2 dots") # Check that the token contains three base64-encoded parts From a00927b3008a2d953fde1d461723a3c92f375eb4 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sun, 22 Feb 2026 15:16:52 +0100 Subject: [PATCH 12/16] fix: preserve debouncing settings for flows with preprocessors (#8043) * fix: preserve debouncing settings for flows with preprocessors Co-Authored-By: Claude Opus 4.5 * Revert "fix: preserve debouncing settings for flows with preprocessors" This reverts commit 3452c1657c449a5ef2a177445e251c5fdb338b34. * feat: add post-preprocessing debounce for flows with preprocessors Co-Authored-By: Claude Opus 4.6 * perf: reuse caller tx for push-time debounce and add stress test Co-Authored-By: Claude Opus 4.6 * test: add exhaustive edge case tests for debouncing behavior Co-Authored-By: Claude Opus 4.5 * perf: optimize debouncing to reduce DB round-trips Co-Authored-By: Claude Opus 4.5 * refactor: replace legacy debounce compat with error logging Co-Authored-By: Claude Opus 4.5 * test: add debounce args accumulation tests Co-Authored-By: Claude Opus 4.5 * test: add end-to-end test for maybe_apply_debouncing arg accumulation Co-Authored-By: Claude Opus 4.5 * chore: update sqlx offline query cache Co-Authored-By: Claude Opus 4.5 * fix: make workmux pane commands idempotent for replay Use git rev-parse --show-toplevel to resolve absolute paths instead of relative cd, so commands work when replayed from within backend/frontend. Co-Authored-By: Claude Opus 4.6 * test: add e2e debounce test script for backend API Comprehensive end-to-end test covering: - Deploy & run scripts rapidly (no debounce with different args) - Redeploy without lock in rapid succession - Debounce with same args (should consolidate) - Debounce with different args (should not consolidate) - Custom debounce key behavior - Git sync debounce + item aggregation (using glob-style ** path filter) Co-Authored-By: Claude Opus 4.6 * chore: update ee-repo-ref to 0fede4b1086bc1456be9cc55b203228c979c5c5e This commit updates the EE repository reference after PR #426 was merged in windmill-ee-private. Previous ee-repo-ref: b5d333370603a6cc7ef70842354cf3be734241b4 New ee-repo-ref: 0fede4b1086bc1456be9cc55b203228c979c5c5e Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: windmill-internal-app[bot] --- .workmux.yaml | 4 +- ...3681f875a5aba22170ca50ec8b578f7fa478b.json | 14 + ...69cdb049f65ec299b0778ce14677728cf6346.json | 26 + ...6c23b4c1a7805c66b70b0fcfb7d33b122a7bf.json | 22 + ...f7dba2e4b04d9535058fab695660a14bf8890.json | 15 + ...8883037bebca4cf75ba459858e4fb197f940b.json | 16 + ...81d41fa47efd4da5bb9bc2d72b9aa1e33617f.json | 22 + ...c08625a55a4dab25c3d9b5ece07e44d14915b.json | 14 + ...6e4c12a5c40f512b70551958178c8b4d6c183.json | 35 - ...c2a3eb4bd22fbe18c34890a3dc4e96e4e6133.json | 22 + ...772cb88a1ab6bbddd97a77e06644ac0f61762.json | 14 + ...dc0227dc3922217fa18f0b71ff0484d65838c.json | 18 - ...f23a74b64573f77dd32189a25b6e8369f147b.json | 14 + ...f9f7c23a1559e7a761db7c2195736b8b30709.json | 14 + ...a1c130b9be1ab4b6a100fffffd687677b9c92.json | 19 + ...fa18d53cbdd16e179749f0aea7980a901b23c.json | 22 + ...2267d5dbbc488bc7ab990c0bda1594bf5ef3a.json | 15 + ...531b5b86862029ab51fdbdd44ec16239108e2.json | 15 - ...9b56ce20c26869ca78d7e17b3504b92ae85b1.json | 12 + ...d74a6582937dfe4759932b63b8e531984008e.json | 34 + ...6e8a4f8f3a9bf04238b33e9caf46836df73d9.json | 35 + ...7e8897e49a4b6affa4e9976680336b6bd3115.json | 15 + ...7b7eefc826af7bf1bb89fd758f7c03c881033.json | 12 + ...43199a18885e1739a5a0e7f6100eab6f3c803.json | 14 + ...24c1f80d717e7c9d76b287da63cb5ee8e8b25.json | 15 + ...1f723b38d1784bd2f1b16d8724f1f1612dcbf.json | 22 + ...212a5bd4039b57fab20b163617e33a4c9dd46.json | 14 + ...7d941819a7202214e0634580bdc2ec30f0b70.json | 22 + ...1b2a1fcc73e520fee79653bb960dc00c3e2db.json | 14 + ...32e97ebefb46be9e58bd3da9067748075311b.json | 35 + ...6b4d3081dfed2e6ff533dbff0bdfd52687cbb.json | 22 + ...28a89f435fc0fb7e55f243b021200f33d2151.json | 14 + ...5afdc3fcbb0eb5f98fd63e2ec89442becbfaa.json | 14 + ...33ef0b98ce5e5fba1f5c1dda3ddd874ff3a39.json | 22 + ...41a4dd87701b0636eb9fa001cd1d9cbcd663b.json | 22 + ...ab5e76296ed437c210942363ba06845f9f963.json | 22 + ...9996410a07bacebc37ed21e0bedf1a33a8fdc.json | 17 + backend/CLAUDE.md | 13 +- backend/ee-repo-ref.txt | 2 +- backend/test_debounce_e2e.sh | 474 +++ backend/windmill-queue/src/jobs.rs | 1 - backend/windmill-queue/tests/debounce_test.rs | 3079 +++++++++++++++++ backend/windmill-worker/src/worker_flow.rs | 65 +- 43 files changed, 4242 insertions(+), 90 deletions(-) create mode 100644 backend/.sqlx/query-0681b850c033619e1b9498376263681f875a5aba22170ca50ec8b578f7fa478b.json create mode 100644 backend/.sqlx/query-1437b432d2c23e30eb05443e83069cdb049f65ec299b0778ce14677728cf6346.json create mode 100644 backend/.sqlx/query-18b6262a60400f2b58ab26615466c23b4c1a7805c66b70b0fcfb7d33b122a7bf.json create mode 100644 backend/.sqlx/query-1af6885dbc5055281acb82b3e57f7dba2e4b04d9535058fab695660a14bf8890.json create mode 100644 backend/.sqlx/query-27f70ebe788cca2e88732d8bf978883037bebca4cf75ba459858e4fb197f940b.json create mode 100644 backend/.sqlx/query-2a95f18e80c55a7e8178a4bd2b781d41fa47efd4da5bb9bc2d72b9aa1e33617f.json create mode 100644 backend/.sqlx/query-4010328a9f1611064f497726b69c08625a55a4dab25c3d9b5ece07e44d14915b.json delete mode 100644 backend/.sqlx/query-454ace9ce391725ef4f4c129cd66e4c12a5c40f512b70551958178c8b4d6c183.json create mode 100644 backend/.sqlx/query-48536968f4173715d4ef8293683c2a3eb4bd22fbe18c34890a3dc4e96e4e6133.json create mode 100644 backend/.sqlx/query-539d661500254e2e346490710f5772cb88a1ab6bbddd97a77e06644ac0f61762.json delete mode 100644 backend/.sqlx/query-5b8c1803f0ccead11517fbc8a9bdc0227dc3922217fa18f0b71ff0484d65838c.json create mode 100644 backend/.sqlx/query-66342c32f7ae0238803cb1896d9f23a74b64573f77dd32189a25b6e8369f147b.json create mode 100644 backend/.sqlx/query-66faba2137791e0cb1353545c06f9f7c23a1559e7a761db7c2195736b8b30709.json create mode 100644 backend/.sqlx/query-79b437ad31ddab94310989b8fb6a1c130b9be1ab4b6a100fffffd687677b9c92.json create mode 100644 backend/.sqlx/query-7ca599330c9913c7e66b27e2ffcfa18d53cbdd16e179749f0aea7980a901b23c.json create mode 100644 backend/.sqlx/query-7ca7dabfe360845a5b57552b0d02267d5dbbc488bc7ab990c0bda1594bf5ef3a.json delete mode 100644 backend/.sqlx/query-8360ab72d60f07dde6ecae599e6531b5b86862029ab51fdbdd44ec16239108e2.json create mode 100644 backend/.sqlx/query-8cf5af21cde4e4de45f995efa2a9b56ce20c26869ca78d7e17b3504b92ae85b1.json create mode 100644 backend/.sqlx/query-8f442110817244aa9533b014aa3d74a6582937dfe4759932b63b8e531984008e.json create mode 100644 backend/.sqlx/query-98033aae3182bde22d5b2ff08ef6e8a4f8f3a9bf04238b33e9caf46836df73d9.json create mode 100644 backend/.sqlx/query-9f50ec7681a1fcd11cb452c7aba7e8897e49a4b6affa4e9976680336b6bd3115.json create mode 100644 backend/.sqlx/query-a057ff9f5998a162ae6de05f6127b7eefc826af7bf1bb89fd758f7c03c881033.json create mode 100644 backend/.sqlx/query-a68754521bf751450602f04dd4243199a18885e1739a5a0e7f6100eab6f3c803.json create mode 100644 backend/.sqlx/query-abb56f78aa39c6b6ae8b0ccb7b724c1f80d717e7c9d76b287da63cb5ee8e8b25.json create mode 100644 backend/.sqlx/query-adb98040c8039e5cc27fe0579941f723b38d1784bd2f1b16d8724f1f1612dcbf.json create mode 100644 backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json create mode 100644 backend/.sqlx/query-b795dc228f93c8b9bedb4a3e7467d941819a7202214e0634580bdc2ec30f0b70.json create mode 100644 backend/.sqlx/query-c1a1ae759ebb84fde3e6d2727991b2a1fcc73e520fee79653bb960dc00c3e2db.json create mode 100644 backend/.sqlx/query-c2347460b73ae9d3167031c263032e97ebefb46be9e58bd3da9067748075311b.json create mode 100644 backend/.sqlx/query-c63a1949247f1618f6b6acee9bf6b4d3081dfed2e6ff533dbff0bdfd52687cbb.json create mode 100644 backend/.sqlx/query-c6d963e5cefeea728414892df9f28a89f435fc0fb7e55f243b021200f33d2151.json create mode 100644 backend/.sqlx/query-c9530931f670eab1208c4a284a55afdc3fcbb0eb5f98fd63e2ec89442becbfaa.json create mode 100644 backend/.sqlx/query-cc309de42a3b630bb83d1b2437633ef0b98ce5e5fba1f5c1dda3ddd874ff3a39.json create mode 100644 backend/.sqlx/query-ccfed494a8d89eb2c88d72738c341a4dd87701b0636eb9fa001cd1d9cbcd663b.json create mode 100644 backend/.sqlx/query-d9400849888dd021b0504b93004ab5e76296ed437c210942363ba06845f9f963.json create mode 100644 backend/.sqlx/query-f8cec94b94098e752f7c71cbe5e9996410a07bacebc37ed21e0bedf1a33a8fdc.json create mode 100755 backend/test_debounce_e2e.sh create mode 100644 backend/windmill-queue/tests/debounce_test.rs diff --git a/.workmux.yaml b/.workmux.yaml index 944679a035..58c85be4ad 100644 --- a/.workmux.yaml +++ b/.workmux.yaml @@ -48,9 +48,9 @@ pre_remove: panes: - command: focus: true - - command: "[ -f .env.local ] && source .env.local; cd backend && PORT=${BACKEND_PORT:-8000} cargo watch -x run" + - command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x run' split: horizontal - - command: "[ -f .env.local ] && source .env.local; cd frontend && npm install && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000}" + - command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/frontend" && npm install && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000}' split: vertical files: diff --git a/backend/.sqlx/query-0681b850c033619e1b9498376263681f875a5aba22170ca50ec8b578f7fa478b.json b/backend/.sqlx/query-0681b850c033619e1b9498376263681f875a5aba22170ca50ec8b578f7fa478b.json new file mode 100644 index 0000000000..91f8c2ce0f --- /dev/null +++ b/backend/.sqlx/query-0681b850c033619e1b9498376263681f875a5aba22170ca50ec8b578f7fa478b.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n SELECT unnest($1::uuid[]), 'test-workspace', now(), 'flow'", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray" + ] + }, + "nullable": [] + }, + "hash": "0681b850c033619e1b9498376263681f875a5aba22170ca50ec8b578f7fa478b" +} diff --git a/backend/.sqlx/query-1437b432d2c23e30eb05443e83069cdb049f65ec299b0778ce14677728cf6346.json b/backend/.sqlx/query-1437b432d2c23e30eb05443e83069cdb049f65ec299b0778ce14677728cf6346.json new file mode 100644 index 0000000000..911d6c3b07 --- /dev/null +++ b/backend/.sqlx/query-1437b432d2c23e30eb05443e83069cdb049f65ec299b0778ce14677728cf6346.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH completed AS (\n INSERT INTO v2_job_completed\n (workspace_id, id, started_at, duration_ms, result,\n flow_status, workflow_as_code_status, status, worker)\n SELECT\n q.workspace_id, q.id, q.started_at,\n (EXTRACT('epoch' FROM now()) - EXTRACT('epoch' FROM COALESCE(q.started_at, now()))) * 1000,\n CASE WHEN q.running\n THEN $3::text::jsonb\n ELSE $4::text::jsonb\n END,\n s.flow_status,\n s.workflow_as_code_status,\n 'skipped'::job_status,\n q.worker\n FROM v2_job_queue q\n LEFT JOIN v2_job_status s ON s.id = q.id\n WHERE q.id = $1\n ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, result = EXCLUDED.result\n RETURNING 1 AS x\n ), _deleted AS (\n DELETE FROM v2_job_queue WHERE id = $1\n ), _logged AS (\n INSERT INTO job_logs (logs, job_id, workspace_id)\n VALUES ($5, $1, $2)\n ON CONFLICT (job_id) DO UPDATE SET logs = concat(job_logs.logs, EXCLUDED.logs)\n )\n SELECT x FROM completed\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "x", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Varchar", + "Text", + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "1437b432d2c23e30eb05443e83069cdb049f65ec299b0778ce14677728cf6346" +} diff --git a/backend/.sqlx/query-18b6262a60400f2b58ab26615466c23b4c1a7805c66b70b0fcfb7d33b122a7bf.json b/backend/.sqlx/query-18b6262a60400f2b58ab26615466c23b4c1a7805c66b70b0fcfb7d33b122a7bf.json new file mode 100644 index 0000000000..05e9f4c7d8 --- /dev/null +++ b/backend/.sqlx/query-18b6262a60400f2b58ab26615466c23b4c1a7805c66b70b0fcfb7d33b122a7bf.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT result::text FROM v2_job_completed WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "result", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "18b6262a60400f2b58ab26615466c23b4c1a7805c66b70b0fcfb7d33b122a7bf" +} diff --git a/backend/.sqlx/query-1af6885dbc5055281acb82b3e57f7dba2e4b04d9535058fab695660a14bf8890.json b/backend/.sqlx/query-1af6885dbc5055281acb82b3e57f7dba2e4b04d9535058fab695660a14bf8890.json new file mode 100644 index 0000000000..780bebfe88 --- /dev/null +++ b/backend/.sqlx/query-1af6885dbc5055281acb82b3e57f7dba2e4b04d9535058fab695660a14bf8890.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n VALUES ($1, $2, now(), 'flow')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "1af6885dbc5055281acb82b3e57f7dba2e4b04d9535058fab695660a14bf8890" +} diff --git a/backend/.sqlx/query-27f70ebe788cca2e88732d8bf978883037bebca4cf75ba459858e4fb197f940b.json b/backend/.sqlx/query-27f70ebe788cca2e88732d8bf978883037bebca4cf75ba459858e4fb197f940b.json new file mode 100644 index 0000000000..3a771e5b19 --- /dev/null +++ b/backend/.sqlx/query-27f70ebe788cca2e88732d8bf978883037bebca4cf75ba459858e4fb197f940b.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path)\n VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "27f70ebe788cca2e88732d8bf978883037bebca4cf75ba459858e4fb197f940b" +} diff --git a/backend/.sqlx/query-2a95f18e80c55a7e8178a4bd2b781d41fa47efd4da5bb9bc2d72b9aa1e33617f.json b/backend/.sqlx/query-2a95f18e80c55a7e8178a4bd2b781d41fa47efd4da5bb9bc2d72b9aa1e33617f.json new file mode 100644 index 0000000000..84a9a67204 --- /dev/null +++ b/backend/.sqlx/query-2a95f18e80c55a7e8178a4bd2b781d41fa47efd4da5bb9bc2d72b9aa1e33617f.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "debounce_batch", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "2a95f18e80c55a7e8178a4bd2b781d41fa47efd4da5bb9bc2d72b9aa1e33617f" +} diff --git a/backend/.sqlx/query-4010328a9f1611064f497726b69c08625a55a4dab25c3d9b5ece07e44d14915b.json b/backend/.sqlx/query-4010328a9f1611064f497726b69c08625a55a4dab25c3d9b5ece07e44d14915b.json new file mode 100644 index 0000000000..fd9c9fe2a6 --- /dev/null +++ b/backend/.sqlx/query-4010328a9f1611064f497726b69c08625a55a4dab25c3d9b5ece07e44d14915b.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path)\n VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', 'ws2', 'f/test/flow')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "4010328a9f1611064f497726b69c08625a55a4dab25c3d9b5ece07e44d14915b" +} diff --git a/backend/.sqlx/query-454ace9ce391725ef4f4c129cd66e4c12a5c40f512b70551958178c8b4d6c183.json b/backend/.sqlx/query-454ace9ce391725ef4f4c129cd66e4c12a5c40f512b70551958178c8b4d6c183.json deleted file mode 100644 index 14a6ab4a40..0000000000 --- a/backend/.sqlx/query-454ace9ce391725ef4f4c129cd66e4c12a5c40f512b70551958178c8b4d6c183.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO debounce_key (job_id, key)\n VALUES ($1, $2)\n ON CONFLICT (key)\n DO UPDATE SET\n previous_job_id = debounce_key.job_id,\n job_id = EXCLUDED.job_id, -- replace current job with new one \n debounced_times = debounce_key.debounced_times + 1 -- evaluated only if conflict,\n -- conflict means there is already existing value,\n -- which means overriding it will also imply adding new entry to v2_job_debounce_batch and thus debouncing the job\n -- so the counter should be incremented\n RETURNING\n debounced_times,\n first_started_at,\n previous_job_id AS job_id_to_debounce\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "debounced_times", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "first_started_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 2, - "name": "job_id_to_debounce", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Varchar" - ] - }, - "nullable": [ - false, - false, - true - ] - }, - "hash": "454ace9ce391725ef4f4c129cd66e4c12a5c40f512b70551958178c8b4d6c183" -} diff --git a/backend/.sqlx/query-48536968f4173715d4ef8293683c2a3eb4bd22fbe18c34890a3dc4e96e4e6133.json b/backend/.sqlx/query-48536968f4173715d4ef8293683c2a3eb4bd22fbe18c34890a3dc4e96e4e6133.json new file mode 100644 index 0000000000..a6a61d6868 --- /dev/null +++ b/backend/.sqlx/query-48536968f4173715d4ef8293683c2a3eb4bd22fbe18c34890a3dc4e96e4e6133.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "WITH ids AS (\n SELECT id as job_id FROM v2_job_debounce_batch WHERE debounce_batch = (\n SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1\n )\n ) SELECT args->>'items' FROM ids LEFT JOIN v2_job ON v2_job.id = ids.job_id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "48536968f4173715d4ef8293683c2a3eb4bd22fbe18c34890a3dc4e96e4e6133" +} diff --git a/backend/.sqlx/query-539d661500254e2e346490710f5772cb88a1ab6bbddd97a77e06644ac0f61762.json b/backend/.sqlx/query-539d661500254e2e346490710f5772cb88a1ab6bbddd97a77e06644ac0f61762.json new file mode 100644 index 0000000000..13d27c1ab2 --- /dev/null +++ b/backend/.sqlx/query-539d661500254e2e346490710f5772cb88a1ab6bbddd97a77e06644ac0f61762.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n SELECT unnest($1::uuid[]), 'test-workspace', now(), 'deno'", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray" + ] + }, + "nullable": [] + }, + "hash": "539d661500254e2e346490710f5772cb88a1ab6bbddd97a77e06644ac0f61762" +} diff --git a/backend/.sqlx/query-5b8c1803f0ccead11517fbc8a9bdc0227dc3922217fa18f0b71ff0484d65838c.json b/backend/.sqlx/query-5b8c1803f0ccead11517fbc8a9bdc0227dc3922217fa18f0b71ff0484d65838c.json deleted file mode 100644 index 8b07d39a36..0000000000 --- a/backend/.sqlx/query-5b8c1803f0ccead11517fbc8a9bdc0227dc3922217fa18f0b71ff0484d65838c.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "WITH job_result AS (\n SELECT result\n FROM v2_job_completed\n WHERE id = $1\n ),\n updated_queue AS (\n UPDATE v2_job_queue\n SET running = false,\n tag = COALESCE($3, tag)\n WHERE id = $2\n )\n UPDATE v2_job\n SET\n tag = COALESCE($3, tag),\n concurrent_limit = COALESCE($4, concurrent_limit),\n concurrency_time_window_s = COALESCE($5, concurrency_time_window_s),\n args = COALESCE(\n CASE\n WHEN job_result.result IS NULL THEN NULL\n WHEN jsonb_typeof(job_result.result) = 'object'\n THEN job_result.result\n WHEN jsonb_typeof(job_result.result) = 'null'\n THEN NULL\n ELSE jsonb_build_object('value', job_result.result)\n END,\n '{}'::jsonb\n ),\n preprocessed = TRUE\n FROM job_result\n WHERE v2_job.id = $2;\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Uuid", - "Varchar", - "Int4", - "Int4" - ] - }, - "nullable": [] - }, - "hash": "5b8c1803f0ccead11517fbc8a9bdc0227dc3922217fa18f0b71ff0484d65838c" -} diff --git a/backend/.sqlx/query-66342c32f7ae0238803cb1896d9f23a74b64573f77dd32189a25b6e8369f147b.json b/backend/.sqlx/query-66342c32f7ae0238803cb1896d9f23a74b64573f77dd32189a25b6e8369f147b.json new file mode 100644 index 0000000000..046f84c48d --- /dev/null +++ b/backend/.sqlx/query-66342c32f7ae0238803cb1896d9f23a74b64573f77dd32189a25b6e8369f147b.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE debounce_key SET first_started_at = now() - interval '20 seconds' WHERE key = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "66342c32f7ae0238803cb1896d9f23a74b64573f77dd32189a25b6e8369f147b" +} diff --git a/backend/.sqlx/query-66faba2137791e0cb1353545c06f9f7c23a1559e7a761db7c2195736b8b30709.json b/backend/.sqlx/query-66faba2137791e0cb1353545c06f9f7c23a1559e7a761db7c2195736b8b30709.json new file mode 100644 index 0000000000..a8f948ff21 --- /dev/null +++ b/backend/.sqlx/query-66faba2137791e0cb1353545c06f9f7c23a1559e7a761db7c2195736b8b30709.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path)\n SELECT unnest($1::uuid[]), 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace', 'f/test/flow'", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray" + ] + }, + "nullable": [] + }, + "hash": "66faba2137791e0cb1353545c06f9f7c23a1559e7a761db7c2195736b8b30709" +} diff --git a/backend/.sqlx/query-79b437ad31ddab94310989b8fb6a1c130b9be1ab4b6a100fffffd687677b9c92.json b/backend/.sqlx/query-79b437ad31ddab94310989b8fb6a1c130b9be1ab4b6a100fffffd687677b9c92.json new file mode 100644 index 0000000000..dd75f876e5 --- /dev/null +++ b/backend/.sqlx/query-79b437ad31ddab94310989b8fb6a1c130b9be1ab4b6a100fffffd687677b9c92.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "WITH job_result AS (\n SELECT result\n FROM v2_job_completed\n WHERE id = $1\n ),\n updated_queue AS (\n UPDATE v2_job_queue\n SET running = false,\n tag = COALESCE($3, tag),\n scheduled_for = COALESCE($6, scheduled_for)\n WHERE id = $2\n )\n UPDATE v2_job\n SET\n tag = COALESCE($3, tag),\n concurrent_limit = COALESCE($4, concurrent_limit),\n concurrency_time_window_s = COALESCE($5, concurrency_time_window_s),\n args = COALESCE(\n CASE\n WHEN job_result.result IS NULL THEN NULL\n WHEN jsonb_typeof(job_result.result) = 'object'\n THEN job_result.result\n WHEN jsonb_typeof(job_result.result) = 'null'\n THEN NULL\n ELSE jsonb_build_object('value', job_result.result)\n END,\n '{}'::jsonb\n ),\n preprocessed = TRUE\n FROM job_result\n WHERE v2_job.id = $2;\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Varchar", + "Int4", + "Int4", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "79b437ad31ddab94310989b8fb6a1c130b9be1ab4b6a100fffffd687677b9c92" +} diff --git a/backend/.sqlx/query-7ca599330c9913c7e66b27e2ffcfa18d53cbdd16e179749f0aea7980a901b23c.json b/backend/.sqlx/query-7ca599330c9913c7e66b27e2ffcfa18d53cbdd16e179749f0aea7980a901b23c.json new file mode 100644 index 0000000000..d15117ba0a --- /dev/null +++ b/backend/.sqlx/query-7ca599330c9913c7e66b27e2ffcfa18d53cbdd16e179749f0aea7980a901b23c.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT logs as \"logs!\" FROM job_logs WHERE job_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "logs!", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true + ] + }, + "hash": "7ca599330c9913c7e66b27e2ffcfa18d53cbdd16e179749f0aea7980a901b23c" +} diff --git a/backend/.sqlx/query-7ca7dabfe360845a5b57552b0d02267d5dbbc488bc7ab990c0bda1594bf5ef3a.json b/backend/.sqlx/query-7ca7dabfe360845a5b57552b0d02267d5dbbc488bc7ab990c0bda1594bf5ef3a.json new file mode 100644 index 0000000000..0a33d674b5 --- /dev/null +++ b/backend/.sqlx/query-7ca7dabfe360845a5b57552b0d02267d5dbbc488bc7ab990c0bda1594bf5ef3a.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n VALUES ($1, $2, now(), 'deno')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "7ca7dabfe360845a5b57552b0d02267d5dbbc488bc7ab990c0bda1594bf5ef3a" +} diff --git a/backend/.sqlx/query-8360ab72d60f07dde6ecae599e6531b5b86862029ab51fdbdd44ec16239108e2.json b/backend/.sqlx/query-8360ab72d60f07dde6ecae599e6531b5b86862029ab51fdbdd44ec16239108e2.json deleted file mode 100644 index 70a904cfc3..0000000000 --- a/backend/.sqlx/query-8360ab72d60f07dde6ecae599e6531b5b86862029ab51fdbdd44ec16239108e2.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO v2_job_debounce_batch (id, debounce_batch)\n -- if it the first one, nextval will be evaluated, otherwise take from the job we will debounce\n SELECT\n $2,\n COALESCE(\n (\n SELECT debounce_batch\n FROM v2_job_debounce_batch\n WHERE id = $1\n LIMIT 1\n ), -- maybe use current batch\n nextval('debounce_batch_seq')\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "8360ab72d60f07dde6ecae599e6531b5b86862029ab51fdbdd44ec16239108e2" -} diff --git a/backend/.sqlx/query-8cf5af21cde4e4de45f995efa2a9b56ce20c26869ca78d7e17b3504b92ae85b1.json b/backend/.sqlx/query-8cf5af21cde4e4de45f995efa2a9b56ce20c26869ca78d7e17b3504b92ae85b1.json new file mode 100644 index 0000000000..347b5a0d11 --- /dev/null +++ b/backend/.sqlx/query-8cf5af21cde4e4de45f995efa2a9b56ce20c26869ca78d7e17b3504b92ae85b1.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO workspace_settings (workspace_id) VALUES ('ws2')", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "8cf5af21cde4e4de45f995efa2a9b56ce20c26869ca78d7e17b3504b92ae85b1" +} diff --git a/backend/.sqlx/query-8f442110817244aa9533b014aa3d74a6582937dfe4759932b63b8e531984008e.json b/backend/.sqlx/query-8f442110817244aa9533b014aa3d74a6582937dfe4759932b63b8e531984008e.json new file mode 100644 index 0000000000..5d29c56fab --- /dev/null +++ b/backend/.sqlx/query-8f442110817244aa9533b014aa3d74a6582937dfe4759932b63b8e531984008e.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT job_id, previous_job_id, debounced_times FROM debounce_key WHERE key = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "job_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "previous_job_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "debounced_times", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + true, + false + ] + }, + "hash": "8f442110817244aa9533b014aa3d74a6582937dfe4759932b63b8e531984008e" +} diff --git a/backend/.sqlx/query-98033aae3182bde22d5b2ff08ef6e8a4f8f3a9bf04238b33e9caf46836df73d9.json b/backend/.sqlx/query-98033aae3182bde22d5b2ff08ef6e8a4f8f3a9bf04238b33e9caf46836df73d9.json new file mode 100644 index 0000000000..7dd6e9ac5d --- /dev/null +++ b/backend/.sqlx/query-98033aae3182bde22d5b2ff08ef6e8a4f8f3a9bf04238b33e9caf46836df73d9.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH dk AS (\n INSERT INTO debounce_key (job_id, key)\n VALUES ($1, $2)\n ON CONFLICT (key)\n DO UPDATE SET\n previous_job_id = debounce_key.job_id,\n job_id = EXCLUDED.job_id,\n debounced_times = debounce_key.debounced_times + 1\n RETURNING\n debounced_times,\n first_started_at,\n previous_job_id AS job_id_to_debounce\n ), _batch AS (\n INSERT INTO v2_job_debounce_batch (id, debounce_batch)\n SELECT\n $1,\n COALESCE(\n (SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = dk.job_id_to_debounce LIMIT 1),\n nextval('debounce_batch_seq')\n )\n FROM dk\n )\n SELECT debounced_times, first_started_at, job_id_to_debounce FROM dk\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "debounced_times", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "first_started_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "job_id_to_debounce", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Varchar" + ] + }, + "nullable": [ + false, + false, + true + ] + }, + "hash": "98033aae3182bde22d5b2ff08ef6e8a4f8f3a9bf04238b33e9caf46836df73d9" +} diff --git a/backend/.sqlx/query-9f50ec7681a1fcd11cb452c7aba7e8897e49a4b6affa4e9976680336b6bd3115.json b/backend/.sqlx/query-9f50ec7681a1fcd11cb452c7aba7e8897e49a4b6affa4e9976680336b6bd3115.json new file mode 100644 index 0000000000..13006d0b30 --- /dev/null +++ b/backend/.sqlx/query-9f50ec7681a1fcd11cb452c7aba7e8897e49a4b6affa4e9976680336b6bd3115.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id)\n VALUES ($1, 'noop', 'deno', 'test-user', 'u/test-user', 'test@windmill.dev', $2)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "9f50ec7681a1fcd11cb452c7aba7e8897e49a4b6affa4e9976680336b6bd3115" +} diff --git a/backend/.sqlx/query-a057ff9f5998a162ae6de05f6127b7eefc826af7bf1bb89fd758f7c03c881033.json b/backend/.sqlx/query-a057ff9f5998a162ae6de05f6127b7eefc826af7bf1bb89fd758f7c03c881033.json new file mode 100644 index 0000000000..383ec3eed5 --- /dev/null +++ b/backend/.sqlx/query-a057ff9f5998a162ae6de05f6127b7eefc826af7bf1bb89fd758f7c03c881033.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO workspace (id, name, owner) VALUES ('ws2', 'Workspace 2', 'test-user')", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "a057ff9f5998a162ae6de05f6127b7eefc826af7bf1bb89fd758f7c03c881033" +} diff --git a/backend/.sqlx/query-a68754521bf751450602f04dd4243199a18885e1739a5a0e7f6100eab6f3c803.json b/backend/.sqlx/query-a68754521bf751450602f04dd4243199a18885e1739a5a0e7f6100eab6f3c803.json new file mode 100644 index 0000000000..8d6e6a2416 --- /dev/null +++ b/backend/.sqlx/query-a68754521bf751450602f04dd4243199a18885e1739a5a0e7f6100eab6f3c803.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job_runtime (id) VALUES ($1)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "a68754521bf751450602f04dd4243199a18885e1739a5a0e7f6100eab6f3c803" +} diff --git a/backend/.sqlx/query-abb56f78aa39c6b6ae8b0ccb7b724c1f80d717e7c9d76b287da63cb5ee8e8b25.json b/backend/.sqlx/query-abb56f78aa39c6b6ae8b0ccb7b724c1f80d717e7c9d76b287da63cb5ee8e8b25.json new file mode 100644 index 0000000000..09862ff013 --- /dev/null +++ b/backend/.sqlx/query-abb56f78aa39c6b6ae8b0ccb7b724c1f80d717e7c9d76b287da63cb5ee8e8b25.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE v2_job_queue SET runnable_settings_handle = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "abb56f78aa39c6b6ae8b0ccb7b724c1f80d717e7c9d76b287da63cb5ee8e8b25" +} diff --git a/backend/.sqlx/query-adb98040c8039e5cc27fe0579941f723b38d1784bd2f1b16d8724f1f1612dcbf.json b/backend/.sqlx/query-adb98040c8039e5cc27fe0579941f723b38d1784bd2f1b16d8724f1f1612dcbf.json new file mode 100644 index 0000000000..df7f24efe3 --- /dev/null +++ b/backend/.sqlx/query-adb98040c8039e5cc27fe0579941f723b38d1784bd2f1b16d8724f1f1612dcbf.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 as x FROM v2_job_completed WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "x", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "adb98040c8039e5cc27fe0579941f723b38d1784bd2f1b16d8724f1f1612dcbf" +} diff --git a/backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json b/backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json new file mode 100644 index 0000000000..a49baeefaf --- /dev/null +++ b/backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray" + ] + }, + "nullable": [] + }, + "hash": "b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46" +} diff --git a/backend/.sqlx/query-b795dc228f93c8b9bedb4a3e7467d941819a7202214e0634580bdc2ec30f0b70.json b/backend/.sqlx/query-b795dc228f93c8b9bedb4a3e7467d941819a7202214e0634580bdc2ec30f0b70.json new file mode 100644 index 0000000000..69c8dbb1ec --- /dev/null +++ b/backend/.sqlx/query-b795dc228f93c8b9bedb4a3e7467d941819a7202214e0634580bdc2ec30f0b70.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 as x FROM v2_job_queue WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "x", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "b795dc228f93c8b9bedb4a3e7467d941819a7202214e0634580bdc2ec30f0b70" +} diff --git a/backend/.sqlx/query-c1a1ae759ebb84fde3e6d2727991b2a1fcc73e520fee79653bb960dc00c3e2db.json b/backend/.sqlx/query-c1a1ae759ebb84fde3e6d2727991b2a1fcc73e520fee79653bb960dc00c3e2db.json new file mode 100644 index 0000000000..b7c771780b --- /dev/null +++ b/backend/.sqlx/query-c1a1ae759ebb84fde3e6d2727991b2a1fcc73e520fee79653bb960dc00c3e2db.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) VALUES ($1, 'ws2', now(), 'flow')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "c1a1ae759ebb84fde3e6d2727991b2a1fcc73e520fee79653bb960dc00c3e2db" +} diff --git a/backend/.sqlx/query-c2347460b73ae9d3167031c263032e97ebefb46be9e58bd3da9067748075311b.json b/backend/.sqlx/query-c2347460b73ae9d3167031c263032e97ebefb46be9e58bd3da9067748075311b.json new file mode 100644 index 0000000000..7d7842d7f4 --- /dev/null +++ b/backend/.sqlx/query-c2347460b73ae9d3167031c263032e97ebefb46be9e58bd3da9067748075311b.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH dk AS (\n INSERT INTO debounce_key (job_id, key)\n VALUES ($1, $2)\n ON CONFLICT (key)\n DO UPDATE SET\n previous_job_id = debounce_key.job_id,\n job_id = EXCLUDED.job_id,\n debounced_times = debounce_key.debounced_times + 1\n RETURNING\n debounced_times,\n first_started_at,\n previous_job_id AS job_id_to_debounce\n ), _batch AS (\n INSERT INTO v2_job_debounce_batch (id, debounce_batch)\n SELECT\n $1,\n COALESCE(\n (SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = dk.job_id_to_debounce LIMIT 1),\n nextval('debounce_batch_seq')\n )\n FROM dk\n )\n SELECT debounced_times, first_started_at, job_id_to_debounce FROM dk\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "debounced_times", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "first_started_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "job_id_to_debounce", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Varchar" + ] + }, + "nullable": [ + false, + false, + true + ] + }, + "hash": "c2347460b73ae9d3167031c263032e97ebefb46be9e58bd3da9067748075311b" +} diff --git a/backend/.sqlx/query-c63a1949247f1618f6b6acee9bf6b4d3081dfed2e6ff533dbff0bdfd52687cbb.json b/backend/.sqlx/query-c63a1949247f1618f6b6acee9bf6b4d3081dfed2e6ff533dbff0bdfd52687cbb.json new file mode 100644 index 0000000000..5d761c2153 --- /dev/null +++ b/backend/.sqlx/query-c63a1949247f1618f6b6acee9bf6b4d3081dfed2e6ff533dbff0bdfd52687cbb.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) as \"count!\" FROM v2_job_queue WHERE id = ANY($1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "UuidArray" + ] + }, + "nullable": [ + null + ] + }, + "hash": "c63a1949247f1618f6b6acee9bf6b4d3081dfed2e6ff533dbff0bdfd52687cbb" +} diff --git a/backend/.sqlx/query-c6d963e5cefeea728414892df9f28a89f435fc0fb7e55f243b021200f33d2151.json b/backend/.sqlx/query-c6d963e5cefeea728414892df9f28a89f435fc0fb7e55f243b021200f33d2151.json new file mode 100644 index 0000000000..94fb53c985 --- /dev/null +++ b/backend/.sqlx/query-c6d963e5cefeea728414892df9f28a89f435fc0fb7e55f243b021200f33d2151.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id)\n SELECT unnest($1::uuid[]), 'noop', 'deno', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace'", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray" + ] + }, + "nullable": [] + }, + "hash": "c6d963e5cefeea728414892df9f28a89f435fc0fb7e55f243b021200f33d2151" +} diff --git a/backend/.sqlx/query-c9530931f670eab1208c4a284a55afdc3fcbb0eb5f98fd63e2ec89442becbfaa.json b/backend/.sqlx/query-c9530931f670eab1208c4a284a55afdc3fcbb0eb5f98fd63e2ec89442becbfaa.json new file mode 100644 index 0000000000..488d3c42bd --- /dev/null +++ b/backend/.sqlx/query-c9530931f670eab1208c4a284a55afdc3fcbb0eb5f98fd63e2ec89442becbfaa.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH _ AS (\n UPDATE debounce_key\n SET debounced_times = 0,\n first_started_at = now(),\n previous_job_id = NULL\n WHERE job_id = $1\n )\n UPDATE v2_job_debounce_batch\n SET debounce_batch = nextval('debounce_batch_seq')\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "c9530931f670eab1208c4a284a55afdc3fcbb0eb5f98fd63e2ec89442becbfaa" +} diff --git a/backend/.sqlx/query-cc309de42a3b630bb83d1b2437633ef0b98ce5e5fba1f5c1dda3ddd874ff3a39.json b/backend/.sqlx/query-cc309de42a3b630bb83d1b2437633ef0b98ce5e5fba1f5c1dda3ddd874ff3a39.json new file mode 100644 index 0000000000..b2e8faee5d --- /dev/null +++ b/backend/.sqlx/query-cc309de42a3b630bb83d1b2437633ef0b98ce5e5fba1f5c1dda3ddd874ff3a39.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) as \"count!\" FROM v2_job_completed WHERE id = ANY($1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "UuidArray" + ] + }, + "nullable": [ + null + ] + }, + "hash": "cc309de42a3b630bb83d1b2437633ef0b98ce5e5fba1f5c1dda3ddd874ff3a39" +} diff --git a/backend/.sqlx/query-ccfed494a8d89eb2c88d72738c341a4dd87701b0636eb9fa001cd1d9cbcd663b.json b/backend/.sqlx/query-ccfed494a8d89eb2c88d72738c341a4dd87701b0636eb9fa001cd1d9cbcd663b.json new file mode 100644 index 0000000000..17034beb5c --- /dev/null +++ b/backend/.sqlx/query-ccfed494a8d89eb2c88d72738c341a4dd87701b0636eb9fa001cd1d9cbcd663b.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = ANY($1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "debounce_batch", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "UuidArray" + ] + }, + "nullable": [ + false + ] + }, + "hash": "ccfed494a8d89eb2c88d72738c341a4dd87701b0636eb9fa001cd1d9cbcd663b" +} diff --git a/backend/.sqlx/query-d9400849888dd021b0504b93004ab5e76296ed437c210942363ba06845f9f963.json b/backend/.sqlx/query-d9400849888dd021b0504b93004ab5e76296ed437c210942363ba06845f9f963.json new file mode 100644 index 0000000000..5e79947b1d --- /dev/null +++ b/backend/.sqlx/query-d9400849888dd021b0504b93004ab5e76296ed437c210942363ba06845f9f963.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = ANY($1) ORDER BY debounce_batch", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "debounce_batch", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "UuidArray" + ] + }, + "nullable": [ + false + ] + }, + "hash": "d9400849888dd021b0504b93004ab5e76296ed437c210942363ba06845f9f963" +} diff --git a/backend/.sqlx/query-f8cec94b94098e752f7c71cbe5e9996410a07bacebc37ed21e0bedf1a33a8fdc.json b/backend/.sqlx/query-f8cec94b94098e752f7c71cbe5e9996410a07bacebc37ed21e0bedf1a33a8fdc.json new file mode 100644 index 0000000000..30127cd1ed --- /dev/null +++ b/backend/.sqlx/query-f8cec94b94098e752f7c71cbe5e9996410a07bacebc37ed21e0bedf1a33a8fdc.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path, args)\n VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Varchar", + "Varchar", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "f8cec94b94098e752f7c71cbe5e9996410a07bacebc37ed21e0bedf1a33a8fdc" +} diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 54550c2dcb..1e4f7e760c 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -44,11 +44,22 @@ Windmill uses a workspace-based architecture with multiple crates: ## Enterprise Features - Enterprise files use the `*_ee.rs` suffix -- Enterprise source is in `windmill-ee-private` folder (sibling directory at `../../windmill-ee-private`), symlinked into each crate's `src/` +- Enterprise source is in `windmill-ee-private` folder (sibling directory at `../../windmill-ee-private` or `~/windmill-ee-private`), symlinked into each crate's `src/` +- The `_ee.rs` files are gitignored in the main repo — they are tracked only in the `windmill-ee-private` repo - You can and should modify `windmill-ee-private` directly when needed (e.g., when creating new crates that need EE code, mirror the package structure there) - Use feature flags: `#[cfg(feature = "enterprise")]` - Isolate enterprise code in separate modules +### EE PR Workflow (MUST DO when modifying `*_ee.rs` files) + +When you modify any `*_ee.rs` file and create a PR on the windmill repo, you **MUST** also: + +1. **Create a matching branch** in the `windmill-ee-private` repo (use the same branch name). If using worktrees, the EE worktree is at `~/windmill-ee-private__worktrees//` +2. **Commit and push** the `_ee.rs` changes in that branch +3. **Create a PR** on `windmill-ee-private` with a link to the companion windmill PR +4. **Update `ee-repo-ref.txt`**: Run `bash write_latest_ee_ref.sh` from `backend/` to write the latest EE commit hash. **Important**: the script may fall back to `~/windmill-ee-private` (main branch) instead of the worktree — verify it wrote the correct commit hash from your branch, not from main. If wrong, manually write the correct hash. +5. **Commit `ee-repo-ref.txt`** in the windmill repo so CI picks up the correct EE ref + ## Code Validation (MUST DO) After making backend changes, you MUST run `cargo check` and fix all errors and warnings before considering the work done. diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index b53996ca96..fbf2a4f9a4 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -5f8105b808f3f0186fdf5132d2ee602d8a14aa17 +0fede4b1086bc1456be9cc55b203228c979c5c5e diff --git a/backend/test_debounce_e2e.sh b/backend/test_debounce_e2e.sh new file mode 100755 index 0000000000..d7088000dd --- /dev/null +++ b/backend/test_debounce_e2e.sh @@ -0,0 +1,474 @@ +#!/usr/bin/env bash +# End-to-end debounce tests against the running backend API +# Usage: BACKEND_PORT=8030 ./test_debounce_e2e.sh +set -uo pipefail + +BASE="http://localhost:${BACKEND_PORT:-8030}/api" +W="admins" +EMAIL="admin@windmill.dev" +PASSWORD="changeme" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +pass=0 +fail=0 + +log_pass() { echo -e "${GREEN}PASS${NC}: $1"; ((pass++)) || true; } +log_fail() { echo -e "${RED}FAIL${NC}: $1 — $2"; ((fail++)) || true; } +log_info() { echo -e "${YELLOW}INFO${NC}: $1"; } + +# Unique suffix for idempotent re-runs +TS=$(date +%s) + +# --- Auth --- +log_info "Logging in..." +TOKEN=$(curl -s "$BASE/auth/login" \ + -H 'Content-Type: application/json' \ + -d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}") + +if [ -z "$TOKEN" ]; then + echo "Failed to login"; exit 1 +fi + +AUTH="Authorization: Bearer $TOKEN" +log_info "Logged in" + +# --- Helpers --- +api() { + # Usage: api METHOD path [data] + local method="$1" path="$2" data="${3:-}" + if [ -n "$data" ]; then + curl -s "$BASE/w/$W/$path" -X "$method" -H "$AUTH" -H 'Content-Type: application/json' -d "$data" + else + curl -s "$BASE/w/$W/$path" -X "$method" -H "$AUTH" + fi +} + +wait_job() { + local job_id="$1" max_wait="${2:-30}" + for _ in $(seq 1 "$max_wait"); do + local r + r=$(api GET "jobs/completed/get_result_maybe/$job_id") + if echo "$r" | jq -e '.completed == true' > /dev/null 2>&1; then + echo "$r"; return 0 + fi + sleep 1 + done + echo '{"completed":false,"error":"timeout"}'; return 1 +} + +BUN_EMPTY_LOCK=$'{"dependencies": {}}\n//bun.lock\n' + +create_script() { + # Usage: create_script path language content [extra_json_fields] + # Note: lock must be non-empty; empty string ("") is treated as None by the backend + # (scripts.rs:798-800), which triggers dependency resolution instead of direct deployment. + # For bun scripts, the lock must contain "//bun.lock" as a split pattern. + local path="$1" lang="$2" content="$3" extra="${4:-}" + local json + json=$(jq -n \ + --arg path "$path" \ + --arg lang "$lang" \ + --arg content "$content" \ + --arg summary "test" \ + --arg desc "test" \ + --arg lock "$BUN_EMPTY_LOCK" \ + '{path: $path, language: $lang, content: $content, summary: $summary, description: $desc, lock: $lock}') + if [ -n "$extra" ]; then + json=$(echo "$json" | jq ". + $extra") + fi + local hash + hash=$(api POST "scripts/create" "$json") + # Small delay for DB visibility after tx commit + sleep 0.2 + echo "$hash" +} + +run_script() { + # Usage: run_script path args_json + api POST "jobs/run/p/$1" "$2" +} + +############################################################################### +# TEST 1: Deploy a script and run it 5 times in close succession +############################################################################### +echo "" +log_info "=== TEST 1: Deploy & run script 5 times rapidly ===" + +P1="u/admin/e2e_simple_$TS" +H1=$(create_script "$P1" "bun" 'export function main(x: number = 0) { return { result: x * 2 }; }') + +if echo "$H1" | grep -qE '^[0-9a-f]{16}$'; then + log_pass "Script created: $H1" +else + log_fail "Script creation" "$H1" +fi + +log_info "Running 5 times rapidly..." +JOB_IDS=() +for i in $(seq 1 5); do + JID=$(run_script "$P1" "{\"x\": $i}") + JOB_IDS+=("$JID") +done +log_info "Jobs: ${JOB_IDS[*]}" + +log_info "Waiting for completion..." +all_ok=true +for i in "${!JOB_IDS[@]}"; do + JID="${JOB_IDS[$i]}" + R=$(wait_job "$JID" 30) + success=$(echo "$R" | jq -r '.success // false') + value=$(echo "$R" | jq -r '.result.result // "null"') + expected=$(( (i + 1) * 2 )) + if [ "$success" = "true" ] && [ "$value" = "$expected" ]; then + log_pass "Job $((i+1)): x=$((i+1)) → $value (correct)" + else + log_fail "Job $((i+1))" "success=$success value=$value expected=$expected" + all_ok=false + fi +done + +if [ "$all_ok" = "true" ]; then + log_pass "All 5 runs completed correctly (no debounce — different args)" +fi + +############################################################################### +# TEST 2: Redeploy script WITHOUT lock in close succession +############################################################################### +echo "" +log_info "=== TEST 2: Redeploy without lock in rapid succession ===" + +P2="u/admin/e2e_nolock_$TS" + +# Deploy 5 versions of the same script without lock → triggers dependency jobs +DEPLOY_HASHES=() +for i in $(seq 1 5); do + content="export function main(x: number = 0) { return { result: x * $i, version: $i }; }" + parent_extra="" + if [ "${#DEPLOY_HASHES[@]}" -gt 0 ]; then + last_hash="${DEPLOY_HASHES[-1]}" + parent_extra="{\"parent_hash\": \"$last_hash\"}" + fi + + # Deploy without lock (omit lock field entirely) + json=$(jq -n \ + --arg path "$P2" \ + --arg content "$content" \ + --arg summary "v$i" \ + --arg desc "test" \ + '{path: $path, language: "bun", content: $content, summary: $summary, description: $desc}') + if [ -n "$parent_extra" ]; then + json=$(echo "$json" | jq ". + $parent_extra") + fi + + hash=$(api POST "scripts/create" "$json") + if echo "$hash" | grep -qE '^[0-9a-f]{16}$'; then + DEPLOY_HASHES+=("$hash") + log_info "Deploy $i: $hash" + else + log_fail "Deploy $i" "$hash" + # If path conflict, the script already exists from a previous version + break + fi + sleep 0.1 +done + +# Wait for dependency resolution +log_info "Waiting 15s for dependency jobs..." +sleep 15 + +# Check the latest script — should have lock resolved +SCRIPT_INFO=$(api GET "scripts/get/p/$P2") +LOCK=$(echo "$SCRIPT_INFO" | jq -r '.lock // "null"') +if [ "$LOCK" != "null" ] && [ -n "$LOCK" ]; then + log_pass "Latest version has lock resolved" +else + log_info "Lock not yet resolved: $LOCK" +fi + +# Run the latest version to verify it works +sleep 0.5 +JID2=$(run_script "$P2" '{"x": 10}') +if echo "$JID2" | grep -qE '^[0-9a-f-]{36}$'; then + R2=$(wait_job "$JID2" 30) + success=$(echo "$R2" | jq -r '.success // false') + if [ "$success" = "true" ]; then + version=$(echo "$R2" | jq -r '.result.version // "?"') + log_pass "Latest version runs: version=$version" + else + err=$(echo "$R2" | jq -r '.result.error.message // "unknown"' 2>/dev/null) + log_fail "Run latest version" "success=false err=$err" + fi +else + log_fail "Run latest version" "bad job id: $JID2" +fi + +############################################################################### +# TEST 3: Script with debounce_delay_s — rapid runs with SAME args +############################################################################### +echo "" +log_info "=== TEST 3: Debounce with same args (should debounce) ===" + +P3="u/admin/e2e_debounce_$TS" +H3=$(create_script "$P3" "bun" \ + 'export function main(x: number = 0) { return { result: x }; }' \ + '{"debounce_delay_s": 3}') + +if echo "$H3" | grep -qE '^[0-9a-f]{16}$'; then + log_pass "Debounce script created: $H3" +else + log_fail "Debounce script creation" "$H3" +fi + +log_info "Running 5 times with same args {x: 42}..." +DEB_IDS=() +for i in $(seq 1 5); do + JID=$(run_script "$P3" '{"x": 42}') + DEB_IDS+=("$JID") + log_info " Run $i: $JID" +done + +log_info "Waiting 10s for debounce delay (3s) + execution..." +sleep 10 + +executed=0 +skipped=0 +for JID in "${DEB_IDS[@]}"; do + if ! echo "$JID" | grep -qE '^[0-9a-f-]{36}$'; then + log_info " Invalid job id: $JID" + continue + fi + R=$(wait_job "$JID" 5 2>/dev/null || echo '{"completed":false}') + completed=$(echo "$R" | jq -r '.completed // false') + success=$(echo "$R" | jq -r '.success // false') + if [ "$completed" = "true" ] && [ "$success" = "true" ]; then + ((executed++)) || true + elif [ "$completed" = "true" ]; then + ((skipped++)) || true + fi +done + +log_info "Results: $executed executed, $skipped skipped out of ${#DEB_IDS[@]}" +if [ "$executed" -eq 1 ] && [ "$skipped" -ge 3 ]; then + log_pass "Debouncing perfect: 1 executed, $skipped skipped" +elif [ "$executed" -le 2 ] && [ "$skipped" -ge 2 ]; then + log_pass "Debouncing working: $executed executed, $skipped skipped" +else + log_fail "Debounce same args" "executed=$executed skipped=$skipped (want ~1 exec, ~4 skip)" +fi + +############################################################################### +# TEST 3b: Different args should NOT debounce against each other +############################################################################### +echo "" +log_info "=== TEST 3b: Debounce with different args (should NOT debounce) ===" + +DIFF_IDS=() +for i in $(seq 1 3); do + JID=$(run_script "$P3" "{\"x\": $((i * 100))}") + DIFF_IDS+=("$JID") +done + +log_info "Waiting 8s..." +sleep 8 + +diff_exec=0 +for JID in "${DIFF_IDS[@]}"; do + if ! echo "$JID" | grep -qE '^[0-9a-f-]{36}$'; then continue; fi + R=$(wait_job "$JID" 5 2>/dev/null || echo '{"completed":false}') + success=$(echo "$R" | jq -r '.success // false') + if [ "$success" = "true" ]; then ((diff_exec++)) || true; fi +done + +if [ "$diff_exec" -eq 3 ]; then + log_pass "Different args: all 3 executed independently" +else + log_fail "Different args" "only $diff_exec/3 executed" +fi + +############################################################################### +# TEST 4: Custom debounce_key with $args interpolation +############################################################################### +echo "" +log_info "=== TEST 4: Custom debounce key ===" + +P4="u/admin/e2e_custom_key_$TS" +H4=$(create_script "$P4" "bun" \ + 'export function main(event_id: string = "", data: string = "") { return { event_id, data }; }' \ + '{"debounce_delay_s": 3, "debounce_key": "event#$args.event_id"}') + +if echo "$H4" | grep -qE '^[0-9a-f]{16}$'; then + log_pass "Custom key script created: $H4" +else + log_fail "Custom key script creation" "$H4" +fi + +# Same event_id → should debounce +log_info "3 runs with same event_id..." +SAME_IDS=() +for i in $(seq 1 3); do + JID=$(run_script "$P4" "{\"event_id\": \"evt_001\", \"data\": \"payload_$i\"}") + SAME_IDS+=("$JID") +done + +# Different event_id → should NOT debounce +JID_DIFF=$(run_script "$P4" '{"event_id": "evt_002", "data": "different"}') + +log_info "Waiting 8s..." +sleep 8 + +same_exec=0 +same_skip=0 +for JID in "${SAME_IDS[@]}"; do + if ! echo "$JID" | grep -qE '^[0-9a-f-]{36}$'; then continue; fi + R=$(wait_job "$JID" 5 2>/dev/null || echo '{"completed":false}') + completed=$(echo "$R" | jq -r '.completed // false') + success=$(echo "$R" | jq -r '.success // false') + if [ "$completed" = "true" ] && [ "$success" = "true" ]; then + data=$(echo "$R" | jq -r '.result.data // "?"') + ((same_exec++)) || true + log_info " Executed: data=$data" + elif [ "$completed" = "true" ]; then + ((same_skip++)) || true + fi +done + +log_info "Same event_id: $same_exec executed, $same_skip skipped" +if [ "$same_exec" -eq 1 ] && [ "$same_skip" -ge 1 ]; then + log_pass "Custom key debounce: same event_id debounced correctly" +elif [ "$same_exec" -le 2 ]; then + log_pass "Custom key debounce working: $same_exec executed, $same_skip skipped" +else + log_fail "Custom key debounce" "exec=$same_exec skip=$same_skip" +fi + +# Check different event_id ran independently +if echo "$JID_DIFF" | grep -qE '^[0-9a-f-]{36}$'; then + R_DIFF=$(wait_job "$JID_DIFF" 10 2>/dev/null || echo '{"completed":false}') + diff_success=$(echo "$R_DIFF" | jq -r '.success // false') + if [ "$diff_success" = "true" ]; then + log_pass "Different event_id: executed independently" + else + log_info "Different event_id: success=$diff_success" + fi +fi + +############################################################################### +# TEST 5: Git sync with bad target — debounced deployment callbacks +############################################################################### +echo "" +log_info "=== TEST 5: Git sync debounce + aggregation ===" + +# Create git repo resource +api POST "resources/create?update_if_exists=true" '{ + "path": "u/admin/e2e_bad_git_repo", + "description": "Bad git repo for testing", + "resource_type": "git_repository", + "value": {"url": "https://github.com/nonexistent/nope.git", "branch": "main", "token": "bad"} +}' > /dev/null 2>&1 +log_info "Created git repo resource" + +# Create a sync script at a folder path where the 2nd segment is a number >= 28103. +# is_script_meets_min_version parses split("/").skip(1).next() as the version number. +# This enables debounce_delay_s=5 and debounce_args_to_accumulate=["items"]. +api POST "folders/create" '{"name": "28103"}' > /dev/null 2>&1 +P5="f/28103/e2e_sync_$TS" +H5=$(create_script "$P5" "bun" \ + 'export function main(repo_url_resource_path: string = "", workspace_id: string = "", items: any[] = [], use_individual_branch: boolean = false, group_by_folder: boolean = false, parent_workspace_id: string = "") { return { synced: items.length, items }; }') + +if echo "$H5" | grep -qE '^[0-9a-f]{16}$'; then + log_pass "Sync script created: $H5" +else + log_fail "Sync script creation" "$H5" +fi + +# Configure git sync with include_path to match deployed scripts. +# Without include_path, path_matches_filters returns false and no DeploymentCallback is created. +api POST "workspaces/edit_git_sync_config" "{ + \"git_sync_settings\": { + \"include_type\": [\"script\"], + \"include_path\": [\"**\"], + \"repositories\": [{ + \"script_path\": \"$P5\", + \"git_repo_resource_path\": \"\$res:u/admin/e2e_bad_git_repo\", + \"use_individual_branch\": false, + \"group_by_folder\": false + }] + } +}" > /dev/null 2>&1 +log_pass "Git sync configured with include_path and versioned folder script path" + +# Deploy 5 scripts rapidly to trigger git sync. +# Scripts are created with lock="" (via create_script), so handle_deployment_metadata +# fires immediately after tx commit (not after dependency resolution). +log_info "Deploying 5 scripts to trigger git sync..." +for i in $(seq 1 5); do + dp="u/admin/e2e_gitsync_${TS}_$i" + create_script "$dp" "bun" "export function main() { return { v: $i }; }" > /dev/null + log_info " Deployed $dp" +done + +# Wait for debounce delay (5s) + execution +log_info "Waiting 15s for debounce (5s) + execution..." +sleep 15 + +# Check deployment callback jobs for our sync script. +# Debounced jobs have is_skipped=true (but success=true), so we use is_skipped to distinguish. +SYNC_JOBS=$(api GET "jobs/completed/list?script_path_exact=$P5&job_kinds=deploymentcallback") +SYNC_TOTAL=$(echo "$SYNC_JOBS" | jq 'length') +SYNC_EXECUTED=$(echo "$SYNC_JOBS" | jq '[.[] | select(.is_skipped != true)] | length') +SYNC_SKIPPED=$(echo "$SYNC_JOBS" | jq '[.[] | select(.is_skipped == true)] | length') + +log_info "Sync jobs: total=$SYNC_TOTAL executed=$SYNC_EXECUTED skipped=$SYNC_SKIPPED" + +if [ "$SYNC_TOTAL" -gt 0 ]; then + # With debouncing (5s delay), rapid deploys should be consolidated. + # All 5 jobs are created but most should be skipped (debounced). + if [ "$SYNC_SKIPPED" -gt 0 ]; then + log_pass "Git sync debouncing: $SYNC_EXECUTED executed, $SYNC_SKIPPED debounced out of $SYNC_TOTAL" + else + log_fail "Git sync debouncing" "No jobs were debounced ($SYNC_TOTAL all executed independently)" + fi + + # Check if items were aggregated in the executed (non-skipped) job(s) + for idx in $(seq 0 $((SYNC_TOTAL - 1))); do + is_skipped=$(echo "$SYNC_JOBS" | jq -r ".[$idx].is_skipped") + [ "$is_skipped" = "true" ] && continue + jid=$(echo "$SYNC_JOBS" | jq -r ".[$idx].id") + r=$(api GET "jobs/completed/get_result/$jid") + items_count=$(echo "$r" | jq '.items | length // 0') + log_info " Executed sync job $jid: items=$items_count" + if [ "$items_count" -gt 1 ]; then + log_pass "Items aggregated: $items_count items in single sync job" + fi + done +else + # Check queued — jobs may still be pending debounce delay + Q=$(api GET "jobs/queue/list?script_path_exact=$P5&job_kinds=deploymentcallback") + QC=$(echo "$Q" | jq 'length') + log_info "No completed sync jobs. $QC queued." + if [ "$QC" -gt 0 ] && [ "$QC" -lt 5 ]; then + log_pass "Git sync debouncing (queued): $QC jobs for 5 deploys" + elif [ "$QC" -eq 0 ]; then + log_fail "Git sync" "No deployment callback jobs found (completed or queued)" + fi +fi + +# Cleanup git sync +api POST "workspaces/edit_git_sync_config" '{"git_sync_settings": null}' > /dev/null 2>&1 +log_info "Git sync config cleared" + +############################################################################### +# Summary +############################################################################### +echo "" +echo "=========================================" +echo -e "Results: ${GREEN}$pass passed${NC}, ${RED}$fail failed${NC}" +echo "=========================================" + +if [ "$fail" -gt 0 ]; then + exit 1 +fi diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 6bf162ed22..007b398c79 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -5368,7 +5368,6 @@ async fn push_inner<'c, 'd>( job_id, &args, &mut tx, - _db, ) .await? } diff --git a/backend/windmill-queue/tests/debounce_test.rs b/backend/windmill-queue/tests/debounce_test.rs new file mode 100644 index 0000000000..9dec28a7ee --- /dev/null +++ b/backend/windmill-queue/tests/debounce_test.rs @@ -0,0 +1,3079 @@ +//! Tests for debouncing logic: both normal (push-time) and post-preprocessing debouncing. +//! +//! Run with: +//! cargo test -p windmill-queue --test debounce_test --features private,enterprise -- --nocapture +//! +//! Requires a live database (migrations are applied automatically by sqlx::test). + +#[cfg(feature = "private")] +mod debounce { + use chrono::Utc; + use serde_json::value::RawValue; + use sqlx::{Pool, Postgres}; + use std::collections::HashMap; + use uuid::Uuid; + use windmill_common::jobs::JobKind; + use windmill_common::runnable_settings::DebouncingSettings; + use windmill_queue::PushArgs; + + /// Helper: insert a minimal job into v2_job + v2_job_queue + v2_job_runtime so debounce can find it. + async fn insert_noop_job(db: &Pool, job_id: Uuid, workspace_id: &str) { + sqlx::query!( + "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id) + VALUES ($1, 'noop', 'deno', 'test-user', 'u/test-user', 'test@windmill.dev', $2)", + job_id, + workspace_id, + ) + .execute(db) + .await + .expect("insert v2_job"); + + sqlx::query!( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) + VALUES ($1, $2, now(), 'deno')", + job_id, + workspace_id, + ) + .execute(db) + .await + .expect("insert v2_job_queue"); + + sqlx::query!("INSERT INTO v2_job_runtime (id) VALUES ($1)", job_id,) + .execute(db) + .await + .expect("insert v2_job_runtime"); + } + + /// Helper: insert a flow job into v2_job + v2_job_queue + v2_job_runtime. + async fn insert_flow_job( + db: &Pool, + job_id: Uuid, + workspace_id: &str, + runnable_path: &str, + ) { + sqlx::query!( + "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path) + VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', $2, $3)", + job_id, + workspace_id, + runnable_path, + ) + .execute(db) + .await + .expect("insert v2_job"); + + sqlx::query!( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) + VALUES ($1, $2, now(), 'flow')", + job_id, + workspace_id, + ) + .execute(db) + .await + .expect("insert v2_job_queue"); + + sqlx::query!("INSERT INTO v2_job_runtime (id) VALUES ($1)", job_id,) + .execute(db) + .await + .expect("insert v2_job_runtime"); + } + + /// Helper: check if a job is completed (exists in v2_job_completed). + async fn is_completed(db: &Pool, job_id: &Uuid) -> bool { + sqlx::query_scalar!("SELECT 1 as x FROM v2_job_completed WHERE id = $1", job_id,) + .fetch_optional(db) + .await + .expect("check completed") + .is_some() + } + + /// Helper: check if a job is still in the queue. + async fn is_queued(db: &Pool, job_id: &Uuid) -> bool { + sqlx::query_scalar!("SELECT 1 as x FROM v2_job_queue WHERE id = $1", job_id,) + .fetch_optional(db) + .await + .expect("check queued") + .is_some() + } + + /// Helper: get the debounce_key entry for a given key. + async fn get_debounce_key(db: &Pool, key: &str) -> Option<(Uuid, Option, i32)> { + sqlx::query!( + "SELECT job_id, previous_job_id, debounced_times FROM debounce_key WHERE key = $1", + key, + ) + .fetch_optional(db) + .await + .expect("get debounce_key") + .map(|r| (r.job_id, r.previous_job_id, r.debounced_times)) + } + + fn empty_args() -> HashMap> { + HashMap::new() + } + + // ========================================================================= + // Tests for maybe_debounce (push-time debouncing) + // ========================================================================= + + /// Test: First job in a debounce batch should set scheduled_for and create debounce_key entry. + /// No previous job should be debounced. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_debounce_first_job(db: Pool) -> anyhow::Result<()> { + let job_id = Uuid::new_v4(); + insert_noop_job(&db, job_id, "test-workspace").await; + + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("test_first_job_key".to_string()), + ..Default::default() + }; + + let mut scheduled_for = None; + let args_hm = empty_args(); + let args = PushArgs::from(&args_hm); + let mut tx = db.begin().await?; + + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script".to_string()), + "test-workspace", + JobKind::Noop, + job_id, + &args, + &mut tx, + ) + .await?; + + tx.commit().await?; + + // scheduled_for should be set to now + 5 seconds + assert!(scheduled_for.is_some(), "scheduled_for should be set"); + let sf = scheduled_for.unwrap(); + let diff = (sf - Utc::now()).num_seconds(); + assert!( + diff >= 3 && diff <= 6, + "scheduled_for should be ~5s in the future, got {diff}s" + ); + + // debounce_key entry should exist with this job + let dk = get_debounce_key(&db, "test_first_job_key").await; + assert!(dk.is_some(), "debounce_key entry should exist"); + let (dk_job_id, dk_prev, dk_times) = dk.unwrap(); + assert_eq!(dk_job_id, job_id); + assert!(dk_prev.is_none(), "no previous job for first in batch"); + assert_eq!(dk_times, 0, "debounced_times should be 0 for first job"); + + // Job should still be in queue (not debounced) + assert!( + is_queued(&db, &job_id).await, + "first job should still be queued" + ); + assert!( + !is_completed(&db, &job_id).await, + "first job should not be completed" + ); + + Ok(()) + } + + /// Test: Second job with the same debounce key should debounce (complete) the first job. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_debounce_second_job_cancels_first(db: Pool) -> anyhow::Result<()> { + let job1 = Uuid::new_v4(); + let job2 = Uuid::new_v4(); + insert_noop_job(&db, job1, "test-workspace").await; + insert_noop_job(&db, job2, "test-workspace").await; + + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("test_cancel_key".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + + // Push job 1 + { + let args = PushArgs::from(&args_hm); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script".to_string()), + "test-workspace", + JobKind::Noop, + job1, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + } + + // Push job 2 with same key - should debounce job 1 + { + let args = PushArgs::from(&args_hm); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script".to_string()), + "test-workspace", + JobKind::Noop, + job2, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + } + + // job1 should be completed (debounced) + assert!( + is_completed(&db, &job1).await, + "job1 should be completed (debounced)" + ); + + // job2 should still be in queue + assert!(is_queued(&db, &job2).await, "job2 should still be in queue"); + + // debounce_key should point to job2 + let dk = get_debounce_key(&db, "test_cancel_key").await.unwrap(); + assert_eq!(dk.0, job2, "debounce_key should point to job2"); + assert_eq!(dk.2, 1, "debounced_times should be 1"); + + Ok(()) + } + + /// Test: 1000 jobs in sequence with the same debounce key — only the last should remain queued. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_debounce_chain_of_1000(db: Pool) -> anyhow::Result<()> { + let n: usize = 1000; + + // Batch-insert all jobs for speed + let jobs: Vec = (0..n).map(|_| Uuid::new_v4()).collect(); + for chunk in jobs.chunks(500) { + let chunk_vec: Vec = chunk.to_vec(); + sqlx::query!( + "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id) + SELECT unnest($1::uuid[]), 'noop', 'deno', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) + SELECT unnest($1::uuid[]), 'test-workspace', now(), 'deno'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", + &chunk_vec, + ) + .execute(&db) + .await?; + } + + let settings = DebouncingSettings { + debounce_delay_s: Some(10), + debounce_key: Some("test_chain_1000_key".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + + for &j in &jobs { + let args = PushArgs::from(&args_hm); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script".to_string()), + "test-workspace", + JobKind::Noop, + j, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + } + + // Only the last job should remain in queue + let queued_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM v2_job_queue WHERE id = ANY($1)", + &jobs, + ) + .fetch_one(&db) + .await?; + assert_eq!( + queued_count, 1, + "exactly 1 job should remain in queue, got {queued_count}" + ); + + // N-1 jobs should be completed (debounced) + let completed_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM v2_job_completed WHERE id = ANY($1)", + &jobs, + ) + .fetch_one(&db) + .await?; + assert_eq!( + completed_count, + (n - 1) as i64, + "{} jobs should be completed (debounced), got {completed_count}", + n - 1 + ); + + // Last job should be the survivor + assert!( + is_queued(&db, &jobs[n - 1]).await, + "last job should still be queued" + ); + + let dk = get_debounce_key(&db, "test_chain_1000_key").await.unwrap(); + assert_eq!(dk.0, jobs[n - 1], "debounce_key should point to last job"); + assert_eq!(dk.2, (n - 1) as i32); + + Ok(()) + } + + /// Test: Different debounce keys should not interfere with each other. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_debounce_different_keys_independent(db: Pool) -> anyhow::Result<()> { + let job_a = Uuid::new_v4(); + let job_b = Uuid::new_v4(); + insert_noop_job(&db, job_a, "test-workspace").await; + insert_noop_job(&db, job_b, "test-workspace").await; + + let args_hm = empty_args(); + + // Push job_a with key "alpha" + { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("alpha".to_string()), + ..Default::default() + }; + let args = PushArgs::from(&args_hm); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script_a".to_string()), + "test-workspace", + JobKind::Noop, + job_a, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + } + + // Push job_b with key "beta" + { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("beta".to_string()), + ..Default::default() + }; + let args = PushArgs::from(&args_hm); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script_b".to_string()), + "test-workspace", + JobKind::Noop, + job_b, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + } + + // Both should still be queued since they have different keys + assert!(is_queued(&db, &job_a).await, "job_a should still be queued"); + assert!(is_queued(&db, &job_b).await, "job_b should still be queued"); + + Ok(()) + } + + /// Test: Debounce key with $args interpolation uses the args to build a unique key. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_debounce_key_with_args_interpolation(db: Pool) -> anyhow::Result<()> { + let job1 = Uuid::new_v4(); + let job2 = Uuid::new_v4(); + let job3 = Uuid::new_v4(); + insert_noop_job(&db, job1, "test-workspace").await; + insert_noop_job(&db, job2, "test-workspace").await; + insert_noop_job(&db, job3, "test-workspace").await; + + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("debounce_$args[tenant_id]".to_string()), + ..Default::default() + }; + + // job1: tenant_id = "A" + { + let mut hm = HashMap::new(); + hm.insert( + "tenant_id".to_string(), + RawValue::from_string("\"A\"".to_string()).unwrap(), + ); + let args = PushArgs::from(&hm); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script".to_string()), + "test-workspace", + JobKind::Noop, + job1, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + } + + // job2: tenant_id = "B" (different key) + { + let mut hm = HashMap::new(); + hm.insert( + "tenant_id".to_string(), + RawValue::from_string("\"B\"".to_string()).unwrap(), + ); + let args = PushArgs::from(&hm); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script".to_string()), + "test-workspace", + JobKind::Noop, + job2, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + } + + // job3: tenant_id = "A" (same key as job1, should debounce job1) + { + let mut hm = HashMap::new(); + hm.insert( + "tenant_id".to_string(), + RawValue::from_string("\"A\"".to_string()).unwrap(), + ); + let args = PushArgs::from(&hm); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script".to_string()), + "test-workspace", + JobKind::Noop, + job3, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + } + + // job1 should be debounced (same key as job3) + assert!( + is_completed(&db, &job1).await, + "job1 should be debounced by job3" + ); + // job2 should still be queued (different key) + assert!( + is_queued(&db, &job2).await, + "job2 should still be queued (different tenant)" + ); + // job3 should still be queued + assert!(is_queued(&db, &job3).await, "job3 should still be queued"); + + Ok(()) + } + + /// Test: When debounce_delay_s is 0 or None, no debouncing should occur. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_no_debounce_when_delay_zero(db: Pool) -> anyhow::Result<()> { + let job1 = Uuid::new_v4(); + let job2 = Uuid::new_v4(); + insert_noop_job(&db, job1, "test-workspace").await; + insert_noop_job(&db, job2, "test-workspace").await; + + let args_hm = empty_args(); + + // delay = 0 + { + let settings = DebouncingSettings { + debounce_delay_s: Some(0), + debounce_key: Some("no_debounce_zero".to_string()), + ..Default::default() + }; + let args = PushArgs::from(&args_hm); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script".to_string()), + "test-workspace", + JobKind::Noop, + job1, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + assert!( + scheduled_for.is_none(), + "scheduled_for should not be set with delay=0" + ); + } + + // delay = None + { + let settings = DebouncingSettings { + debounce_delay_s: None, + debounce_key: Some("no_debounce_none".to_string()), + ..Default::default() + }; + let args = PushArgs::from(&args_hm); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script".to_string()), + "test-workspace", + JobKind::Noop, + job2, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + assert!( + scheduled_for.is_none(), + "scheduled_for should not be set with delay=None" + ); + } + + // Both should still be queued + assert!(is_queued(&db, &job1).await); + assert!(is_queued(&db, &job2).await); + + Ok(()) + } + + /// Test: max_total_debounces_amount limit - debounce batch resets when exceeded. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_debounce_max_count_limit(db: Pool) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("count_limit_key".to_string()), + max_total_debounces_amount: Some(2), + ..Default::default() + }; + let args_hm = empty_args(); + + // Push 4 jobs: after the 3rd debounce (exceeding limit of 2), batch should reset + let mut jobs = Vec::new(); + for _ in 0..4 { + let job_id = Uuid::new_v4(); + insert_noop_job(&db, job_id, "test-workspace").await; + jobs.push(job_id); + } + + for &j in &jobs { + let args = PushArgs::from(&args_hm); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &Some("f/test/script".to_string()), + "test-workspace", + JobKind::Noop, + j, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + } + + // The debounce_key entry should still exist + let dk = get_debounce_key(&db, "count_limit_key").await; + assert!(dk.is_some(), "debounce_key entry should exist"); + + Ok(()) + } + + // ========================================================================= + // Tests for maybe_debounce_post_preprocessing + // ========================================================================= + + /// Test: Post-preprocessing debounce with first job returns scheduled_for. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_debounce_first_job(db: Pool) -> anyhow::Result<()> { + let flow_id = Uuid::new_v4(); + insert_flow_job(&db, flow_id, "test-workspace", "f/test/flow").await; + + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_first_key".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + let args = PushArgs::from(&args_hm); + + let result = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow_id, + &args, + &db, + ) + .await?; + + // Should return a scheduled_for value + assert!( + result.is_some(), + "should return scheduled_for for first job" + ); + let sf = result.unwrap(); + let diff = (sf - Utc::now()).num_seconds(); + assert!( + diff >= 3 && diff <= 6, + "scheduled_for should be ~5s in future, got {diff}s" + ); + + // debounce_key should be created + let dk = get_debounce_key(&db, "pp_first_key").await; + assert!(dk.is_some(), "debounce_key entry should exist"); + let (dk_job_id, _, _) = dk.unwrap(); + assert_eq!(dk_job_id, flow_id); + + Ok(()) + } + + /// Test: Post-preprocessing debounce with second job debounces the first. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_debounce_second_cancels_first( + db: Pool, + ) -> anyhow::Result<()> { + let flow1 = Uuid::new_v4(); + let flow2 = Uuid::new_v4(); + insert_flow_job(&db, flow1, "test-workspace", "f/test/flow").await; + insert_flow_job(&db, flow2, "test-workspace", "f/test/flow").await; + + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_cancel_key".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + + // First flow + { + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow1, + &args, + &db, + ) + .await?; + } + + // Second flow - should debounce the first + { + let args = PushArgs::from(&args_hm); + let result = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow2, + &args, + &db, + ) + .await?; + assert!(result.is_some(), "should return scheduled_for"); + } + + // flow1 should be completed (debounced) + assert!( + is_completed(&db, &flow1).await, + "flow1 should be completed (debounced by flow2)" + ); + + // flow2 should still be in queue + assert!(is_queued(&db, &flow2).await, "flow2 should still be queued"); + + // debounce_key should point to flow2 + let dk = get_debounce_key(&db, "pp_cancel_key").await.unwrap(); + assert_eq!(dk.0, flow2, "debounce_key should point to flow2"); + assert_eq!(dk.2, 1, "debounced_times should be 1"); + + Ok(()) + } + + /// Test: Post-preprocessing debounce with args-based key differentiates by preprocessed args. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_debounce_args_differentiation( + db: Pool, + ) -> anyhow::Result<()> { + let flow_a = Uuid::new_v4(); + let flow_b = Uuid::new_v4(); + let flow_a2 = Uuid::new_v4(); + insert_flow_job(&db, flow_a, "test-workspace", "f/test/flow").await; + insert_flow_job(&db, flow_b, "test-workspace", "f/test/flow").await; + insert_flow_job(&db, flow_a2, "test-workspace", "f/test/flow").await; + + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_$args[region]".to_string()), + ..Default::default() + }; + + // flow_a: region = "us" + { + let mut hm = HashMap::new(); + hm.insert( + "region".to_string(), + RawValue::from_string("\"us\"".to_string()).unwrap(), + ); + let args = PushArgs::from(&hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow_a, + &args, + &db, + ) + .await?; + } + + // flow_b: region = "eu" (different key, no debounce) + { + let mut hm = HashMap::new(); + hm.insert( + "region".to_string(), + RawValue::from_string("\"eu\"".to_string()).unwrap(), + ); + let args = PushArgs::from(&hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow_b, + &args, + &db, + ) + .await?; + } + + // flow_a2: region = "us" (same key as flow_a, should debounce flow_a) + { + let mut hm = HashMap::new(); + hm.insert( + "region".to_string(), + RawValue::from_string("\"us\"".to_string()).unwrap(), + ); + let args = PushArgs::from(&hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow_a2, + &args, + &db, + ) + .await?; + } + + // flow_a should be debounced (same region as flow_a2) + assert!( + is_completed(&db, &flow_a).await, + "flow_a should be debounced by flow_a2" + ); + // flow_b should be queued (different region) + assert!( + is_queued(&db, &flow_b).await, + "flow_b should still be queued" + ); + // flow_a2 should be queued + assert!( + is_queued(&db, &flow_a2).await, + "flow_a2 should still be queued" + ); + + Ok(()) + } + + /// Test: Post-preprocessing debounce returns None when delay is zero. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_no_debounce_zero_delay( + db: Pool, + ) -> anyhow::Result<()> { + let flow_id = Uuid::new_v4(); + insert_flow_job(&db, flow_id, "test-workspace", "f/test/flow").await; + + let settings = DebouncingSettings { + debounce_delay_s: Some(0), + debounce_key: Some("pp_zero_delay".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + let args = PushArgs::from(&args_hm); + + let result = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow_id, + &args, + &db, + ) + .await?; + + assert!(result.is_none(), "should return None when delay is 0"); + Ok(()) + } + + /// Test: Post-preprocessing debounce returns None when delay is None. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_no_debounce_no_delay( + db: Pool, + ) -> anyhow::Result<()> { + let flow_id = Uuid::new_v4(); + insert_flow_job(&db, flow_id, "test-workspace", "f/test/flow").await; + + let settings = DebouncingSettings::default(); + let args_hm = empty_args(); + let args = PushArgs::from(&args_hm); + + let result = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow_id, + &args, + &db, + ) + .await?; + + assert!(result.is_none(), "should return None with default settings"); + Ok(()) + } + + /// Test: Post-preprocessing debounce chain of 1000 jobs — only the last should remain queued. + /// This verifies debouncing works correctly at scale with sequential debounce operations. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_debounce_chain_1000(db: Pool) -> anyhow::Result<()> { + let n: usize = 1000; + + // Batch-insert all jobs using raw SQL for speed + let uuids: Vec = (0..n).map(|_| Uuid::new_v4()).collect(); + for chunk in uuids.chunks(500) { + let chunk_vec: Vec = chunk.to_vec(); + sqlx::query!( + "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path) + SELECT unnest($1::uuid[]), 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace', 'f/test/flow'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) + SELECT unnest($1::uuid[]), 'test-workspace', now(), 'flow'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", + &chunk_vec, + ) + .execute(&db) + .await?; + } + + let settings = DebouncingSettings { + debounce_delay_s: Some(10), + debounce_key: Some("pp_chain_1000_key".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + + for &j in &uuids { + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + j, + &args, + &db, + ) + .await?; + } + + // Only the last job should remain in queue + let queued_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM v2_job_queue WHERE id = ANY($1)", + &uuids, + ) + .fetch_one(&db) + .await?; + assert_eq!( + queued_count, 1, + "exactly 1 job should remain in queue, got {queued_count}" + ); + + // N-1 jobs should be completed (debounced) + let completed_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM v2_job_completed WHERE id = ANY($1)", + &uuids, + ) + .fetch_one(&db) + .await?; + assert_eq!( + completed_count, + (n - 1) as i64, + "n-1 jobs should be completed (debounced), got {completed_count}" + ); + + let dk = get_debounce_key(&db, "pp_chain_1000_key").await.unwrap(); + assert_eq!(dk.0, uuids[n - 1], "debounce_key should point to last job"); + assert_eq!(dk.2, (n - 1) as i32); + + Ok(()) + } + + /// Test: Post-preprocessing debounce with max count limit resets the batch. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_debounce_max_count_resets( + db: Pool, + ) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_max_count_key".to_string()), + max_total_debounces_amount: Some(2), + ..Default::default() + }; + let args_hm = empty_args(); + + // Push 4 jobs. After 3rd debounce (exceeding limit of 2), batch should reset. + let mut jobs = Vec::new(); + let mut results = Vec::new(); + for _ in 0..4 { + let id = Uuid::new_v4(); + insert_flow_job(&db, id, "test-workspace", "f/test/flow").await; + jobs.push(id); + } + + for &j in &jobs { + let args = PushArgs::from(&args_hm); + let result = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + j, + &args, + &db, + ) + .await?; + results.push(result); + } + + // First job always gets scheduled_for + assert!(results[0].is_some(), "first job should get scheduled_for"); + + // Jobs 2 and 3 should also get scheduled_for (debouncing within limit) + assert!(results[1].is_some(), "second job should get scheduled_for"); + assert!(results[2].is_some(), "third job should get scheduled_for"); + + // Job 4 (the one that exceeds the limit): when limit is exceeded, + // the batch resets and the job executes immediately (no scheduled_for delay) + // The exact behavior depends on whether the limit check happens before or after the + // new job is counted. Let's just verify the debounce_key is reset. + let dk = get_debounce_key(&db, "pp_max_count_key").await.unwrap(); + // debounced_times should have been reset at some point + assert!(dk.0 == jobs[3], "debounce_key should point to last job"); + + Ok(()) + } + + /// Test: 1000 concurrent debounce operations with different keys — no contention or deadlocks. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_concurrent_different_keys_1000( + db: Pool, + ) -> anyhow::Result<()> { + let n: usize = 1000; + + // Batch-insert all flow jobs upfront + let flow_ids: Vec = (0..n).map(|_| Uuid::new_v4()).collect(); + for chunk in flow_ids.chunks(500) { + let chunk_vec: Vec = chunk.to_vec(); + sqlx::query!( + "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path) + SELECT unnest($1::uuid[]), 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace', 'f/test/flow'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) + SELECT unnest($1::uuid[]), 'test-workspace', now(), 'flow'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", + &chunk_vec, + ) + .execute(&db) + .await?; + } + + // Fire all debounce calls concurrently, each with a unique key + let mut handles = Vec::new(); + for (i, &flow_id) in flow_ids.iter().enumerate() { + let db = db.clone(); + let handle = tokio::spawn(async move { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some(format!("concurrent_key_{i}")), + ..Default::default() + }; + let args_hm: HashMap> = HashMap::new(); + let args = PushArgs::from(&args_hm); + + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow_id, + &args, + &db, + ) + .await + }); + handles.push(handle); + } + + let mut error_count = 0; + for handle in handles { + match handle.await? { + Ok(result) => { + assert!(result.is_some(), "should return scheduled_for"); + } + Err(e) => { + eprintln!("Concurrent debounce error: {e:#}"); + error_count += 1; + } + } + } + assert_eq!(error_count, 0, "no errors expected, got {error_count}"); + + // All jobs should still be in queue (each has a unique key, no debouncing between them) + let queued_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM v2_job_queue WHERE id = ANY($1)", + &flow_ids, + ) + .fetch_one(&db) + .await?; + assert_eq!( + queued_count, n as i64, + "all {n} jobs should remain in queue, got {queued_count}" + ); + + Ok(()) + } + + /// Test: 1000 concurrent debounce operations with the SAME key — verifies no deadlocks + /// and exactly 1 job survives in the queue. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_concurrent_same_key_1000( + db: Pool, + ) -> anyhow::Result<()> { + let n: usize = 1000; + + // Batch-insert all flow jobs upfront + let flow_ids: Vec = (0..n).map(|_| Uuid::new_v4()).collect(); + for chunk in flow_ids.chunks(500) { + let chunk_vec: Vec = chunk.to_vec(); + sqlx::query!( + "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path) + SELECT unnest($1::uuid[]), 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace', 'f/test/flow'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) + SELECT unnest($1::uuid[]), 'test-workspace', now(), 'flow'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", + &chunk_vec, + ) + .execute(&db) + .await?; + } + + // Fire all debounce calls concurrently, all sharing the same key + let mut handles = Vec::new(); + for &flow_id in &flow_ids { + let db = db.clone(); + let handle = tokio::spawn(async move { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("shared_concurrent_key_1000".to_string()), + ..Default::default() + }; + let args_hm: HashMap> = HashMap::new(); + let args = PushArgs::from(&args_hm); + + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow_id, + &args, + &db, + ) + .await + }); + handles.push(handle); + } + + let mut success_count = 0; + let mut error_count = 0; + for handle in handles { + match handle.await? { + Ok(_) => success_count += 1, + Err(e) => { + eprintln!("Concurrent debounce error: {e:#}"); + error_count += 1; + } + } + } + + assert_eq!(error_count, 0, "no errors expected, got {error_count}"); + assert_eq!(success_count, n, "all {n} debounce calls should succeed"); + + // Only 1 job should remain in queue, rest should be debounced + let queued_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM v2_job_queue WHERE id = ANY($1)", + &flow_ids, + ) + .fetch_one(&db) + .await?; + assert_eq!( + queued_count, 1, + "exactly 1 job should remain in queue, got {queued_count}" + ); + + let completed_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM v2_job_completed WHERE id = ANY($1)", + &flow_ids, + ) + .fetch_one(&db) + .await?; + assert_eq!( + completed_count, + (n - 1) as i64, + "{} jobs should be completed (debounced), got {completed_count}", + n - 1 + ); + + Ok(()) + } + + // ========================================================================= + // Edge case tests: timing, limits, batch behavior, scheduled_for + // ========================================================================= + + /// Test: scheduled_for is set to approximately now + delay_seconds. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_push_scheduled_for_value(db: Pool) -> anyhow::Result<()> { + let job_id = Uuid::new_v4(); + insert_noop_job(&db, job_id, "test-workspace").await; + + let settings = DebouncingSettings { + debounce_delay_s: Some(30), + debounce_key: Some("scheduled_for_test".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + let args = PushArgs::from(&args_hm); + + let before = Utc::now(); + let mut scheduled_for = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &None, + "test-workspace", + JobKind::Noop, + job_id, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + let after = Utc::now(); + + let sf = scheduled_for.expect("scheduled_for should be set"); + let expected_min = before + chrono::Duration::seconds(30); + let expected_max = after + chrono::Duration::seconds(30); + assert!( + sf >= expected_min && sf <= expected_max, + "scheduled_for ({sf}) should be between {expected_min} and {expected_max}" + ); + + Ok(()) + } + + /// Test: post-preprocessing scheduled_for is set to approximately now + delay_seconds. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_scheduled_for_value(db: Pool) -> anyhow::Result<()> { + let flow_id = Uuid::new_v4(); + insert_flow_job(&db, flow_id, "test-workspace", "f/test/flow").await; + + let settings = DebouncingSettings { + debounce_delay_s: Some(45), + debounce_key: Some("pp_scheduled_for_test".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + let args = PushArgs::from(&args_hm); + + let before = Utc::now(); + let result = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow_id, + &args, + &db, + ) + .await?; + let after = Utc::now(); + + let sf = result.expect("should return scheduled_for"); + let expected_min = before + chrono::Duration::seconds(45); + let expected_max = after + chrono::Duration::seconds(45); + assert!( + sf >= expected_min && sf <= expected_max, + "scheduled_for ({sf}) should be between {expected_min} and {expected_max}" + ); + + Ok(()) + } + + /// Test: push-time does NOT set scheduled_for if one is already provided (uses .or()). + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_push_preserves_existing_scheduled_for(db: Pool) -> anyhow::Result<()> { + let job_id = Uuid::new_v4(); + insert_noop_job(&db, job_id, "test-workspace").await; + + let settings = DebouncingSettings { + debounce_delay_s: Some(30), + debounce_key: Some("preserve_sf_test".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + let args = PushArgs::from(&args_hm); + + let preset = Utc::now() + chrono::Duration::seconds(999); + let mut scheduled_for = Some(preset); + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &None, + "test-workspace", + JobKind::Noop, + job_id, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + + assert_eq!( + scheduled_for, + Some(preset), + "existing scheduled_for should be preserved" + ); + + Ok(()) + } + + /// Test: max_total_debouncing_time causes batch reset when exceeded. + /// Uses direct DB manipulation to set first_started_at in the past. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_max_time_exceeded(db: Pool) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_time_limit_key".to_string()), + max_total_debouncing_time: Some(10), // 10 seconds max + ..Default::default() + }; + let args_hm = empty_args(); + + // Job 1: first in batch + let job1 = Uuid::new_v4(); + insert_flow_job(&db, job1, "test-workspace", "f/test/flow").await; + let args = PushArgs::from(&args_hm); + let r1 = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job1, + &args, + &db, + ) + .await?; + assert!(r1.is_some(), "first job should get scheduled_for"); + + // Force first_started_at to 20 seconds ago to simulate time exceeding the limit + sqlx::query!( + "UPDATE debounce_key SET first_started_at = now() - interval '20 seconds' WHERE key = $1", + "pp_time_limit_key" + ) + .execute(&db) + .await?; + + // Job 2: should trigger time limit exceeded → batch reset, no debouncing + let job2 = Uuid::new_v4(); + insert_flow_job(&db, job2, "test-workspace", "f/test/flow").await; + let args = PushArgs::from(&args_hm); + let r2 = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job2, + &args, + &db, + ) + .await?; + // When limit is exceeded, the function resets and returns None (execute immediately) + assert!( + r2.is_none(), + "should return None when time limit is exceeded" + ); + + // Verify the batch was reset: debounced_times should be 0 + let dk = get_debounce_key(&db, "pp_time_limit_key").await.unwrap(); + assert_eq!(dk.2, 0, "debounced_times should be reset to 0"); + + // Job 1 should NOT be completed (time limit reset skips debouncing the previous job) + assert!( + is_queued(&db, &job1).await, + "job1 should still be queued (time limit reset doesn't debounce)" + ); + + Ok(()) + } + + /// Test: push-time max_total_debouncing_time causes batch reset when exceeded. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_push_max_time_exceeded(db: Pool) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("push_time_limit_key".to_string()), + max_total_debouncing_time: Some(10), + ..Default::default() + }; + let args_hm = empty_args(); + + // Job 1: first in batch + let job1 = Uuid::new_v4(); + insert_noop_job(&db, job1, "test-workspace").await; + let args = PushArgs::from(&args_hm); + let mut sf = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut sf, + &None, + "test-workspace", + JobKind::Noop, + job1, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + assert!(sf.is_some(), "first job should get scheduled_for"); + + // Force first_started_at to 20 seconds ago + sqlx::query!( + "UPDATE debounce_key SET first_started_at = now() - interval '20 seconds' WHERE key = $1", + "push_time_limit_key" + ) + .execute(&db) + .await?; + + // Job 2: should trigger time limit exceeded + let job2 = Uuid::new_v4(); + insert_noop_job(&db, job2, "test-workspace").await; + let args = PushArgs::from(&args_hm); + let mut sf2 = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut sf2, + &None, + "test-workspace", + JobKind::Noop, + job2, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + // scheduled_for is still set (push-time doesn't clear it on limit exceed) + // but the batch should be reset + let dk = get_debounce_key(&db, "push_time_limit_key").await.unwrap(); + assert_eq!(dk.2, 0, "debounced_times should be reset to 0"); + + Ok(()) + } + + /// Test: max_count boundary — at exactly the limit, debouncing still works. + /// One over the limit triggers reset. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_max_count_exact_boundary( + db: Pool, + ) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_count_boundary_key".to_string()), + max_total_debounces_amount: Some(3), + ..Default::default() + }; + let args_hm = empty_args(); + + let mut jobs = Vec::new(); + let mut results = Vec::new(); + // Push 5 jobs: job 1 (no debounce), jobs 2-4 (debounce, count 1-3), job 5 (count 4 > limit 3 → reset) + for _ in 0..5 { + let id = Uuid::new_v4(); + insert_flow_job(&db, id, "test-workspace", "f/test/flow").await; + jobs.push(id); + } + + for &j in &jobs { + let args = PushArgs::from(&args_hm); + let result = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + j, + &args, + &db, + ) + .await?; + results.push(result); + } + + // Jobs 1-4 should return Some (scheduled_for) — debouncing within limit + for (i, r) in results.iter().enumerate().take(4) { + assert!( + r.is_some(), + "job {} should get scheduled_for (within limit)", + i + 1 + ); + } + + // Job 5 (debounced_times=4, exceeds limit=3) should return None (batch reset) + assert!( + results[4].is_none(), + "job 5 should return None (limit exceeded, batch reset)" + ); + + // After reset, debounced_times should be 0 + let dk = get_debounce_key(&db, "pp_count_boundary_key") + .await + .unwrap(); + assert_eq!(dk.2, 0, "debounced_times should be reset to 0 after limit"); + + Ok(()) + } + + /// Test: after a max_count reset, a new batch starts fresh and debouncing works again. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_max_count_reset_new_batch( + db: Pool, + ) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_reset_cycle_key".to_string()), + max_total_debounces_amount: Some(2), + ..Default::default() + }; + let args_hm = empty_args(); + + // Limit check is `debounced_times > max`, so with max=2 we need 4 jobs + // to trigger reset (debounced_times=3 on the 4th job, 3>2=true). + // Cycle 1: jobs 1-4 (job 4 exceeds limit → reset) + let mut cycle1 = Vec::new(); + for _ in 0..4 { + let id = Uuid::new_v4(); + insert_flow_job(&db, id, "test-workspace", "f/test/flow").await; + cycle1.push(id); + } + let mut cycle1_results = Vec::new(); + for &j in &cycle1 { + let args = PushArgs::from(&args_hm); + let r = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + j, + &args, + &db, + ) + .await?; + cycle1_results.push(r); + } + assert!(cycle1_results[0].is_some(), "cycle1 job1 scheduled"); + assert!(cycle1_results[1].is_some(), "cycle1 job2 scheduled"); + assert!( + cycle1_results[2].is_some(), + "cycle1 job3 scheduled (at limit)" + ); + assert!( + cycle1_results[3].is_none(), + "cycle1 job4 should reset (over limit)" + ); + + // Verify debounced_times is reset to 0 + let dk = get_debounce_key(&db, "pp_reset_cycle_key").await.unwrap(); + assert_eq!(dk.2, 0, "debounced_times should be 0 after reset"); + + // Cycle 2: jobs 5-8 (new batch, should debounce independently) + let mut cycle2 = Vec::new(); + for _ in 0..4 { + let id = Uuid::new_v4(); + insert_flow_job(&db, id, "test-workspace", "f/test/flow").await; + cycle2.push(id); + } + let mut cycle2_results = Vec::new(); + for &j in &cycle2 { + let args = PushArgs::from(&args_hm); + let r = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + j, + &args, + &db, + ) + .await?; + cycle2_results.push(r); + } + // After cycle 1 reset, debounced_times=0. Cycle 2's first job hits ON CONFLICT + // and increments to 1 (unlike cycle 1's first job which was a fresh insert at 0). + // So cycle 2 reaches the limit one job sooner: + // job5: dt=1, job6: dt=2, job7: dt=3 (>2 → reset), job8: dt=1 + assert!(cycle2_results[0].is_some(), "cycle2 job1 scheduled (dt=1)"); + assert!(cycle2_results[1].is_some(), "cycle2 job2 scheduled (dt=2)"); + assert!( + cycle2_results[2].is_none(), + "cycle2 job3 should reset (dt=3 > 2)" + ); + assert!( + cycle2_results[3].is_some(), + "cycle2 job4 scheduled (fresh after reset, dt=1)" + ); + + Ok(()) + } + + /// Test: combined max_count AND max_time — whichever triggers first resets the batch. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_combined_count_and_time_limits( + db: Pool, + ) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_combined_limits_key".to_string()), + max_total_debounces_amount: Some(100), // high count limit + max_total_debouncing_time: Some(10), // low time limit + ..Default::default() + }; + let args_hm = empty_args(); + + // Job 1: start batch + let job1 = Uuid::new_v4(); + insert_flow_job(&db, job1, "test-workspace", "f/test/flow").await; + let args = PushArgs::from(&args_hm); + let r1 = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job1, + &args, + &db, + ) + .await?; + assert!(r1.is_some(), "first job should get scheduled_for"); + + // Force time to exceed limit (count is still 1, well under 100) + sqlx::query!( + "UPDATE debounce_key SET first_started_at = now() - interval '20 seconds' WHERE key = $1", + "pp_combined_limits_key" + ) + .execute(&db) + .await?; + + // Job 2: time limit should trigger even though count is low + let job2 = Uuid::new_v4(); + insert_flow_job(&db, job2, "test-workspace", "f/test/flow").await; + let args = PushArgs::from(&args_hm); + let r2 = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job2, + &args, + &db, + ) + .await?; + assert!( + r2.is_none(), + "time limit should trigger reset even with low count" + ); + + Ok(()) + } + + /// Test: debounce batch IDs are consistent within a batch. + /// All jobs in the same debounce batch should share the same batch number. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_batch_id_consistency( + db: Pool, + ) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_batch_id_test".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + + let mut jobs = Vec::new(); + for _ in 0..5 { + let id = Uuid::new_v4(); + insert_flow_job(&db, id, "test-workspace", "f/test/flow").await; + jobs.push(id); + } + + for &j in &jobs { + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + j, + &args, + &db, + ) + .await?; + } + + // All jobs should have the same debounce_batch + let batches: Vec = sqlx::query_scalar!( + "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = ANY($1) ORDER BY debounce_batch", + &jobs, + ) + .fetch_all(&db) + .await?; + + assert_eq!(batches.len(), 5, "all 5 jobs should have batch entries"); + let first = batches[0]; + assert!( + batches.iter().all(|b| *b == first), + "all jobs in same debounce batch should have the same batch ID, got {:?}", + batches + ); + + Ok(()) + } + + /// Test: after a max_count reset, the new batch gets a different batch ID. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_batch_id_changes_on_reset( + db: Pool, + ) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_batch_reset_id_test".to_string()), + max_total_debounces_amount: Some(2), + ..Default::default() + }; + let args_hm = empty_args(); + + // Batch 1: jobs 1-4 (job 4 triggers reset at debounced_times=3 > 2) + let mut batch1_jobs = Vec::new(); + for _ in 0..4 { + let id = Uuid::new_v4(); + insert_flow_job(&db, id, "test-workspace", "f/test/flow").await; + batch1_jobs.push(id); + } + for &j in &batch1_jobs { + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + j, + &args, + &db, + ) + .await?; + } + + // Batch 2: jobs 5-6 (new batch after reset) + let mut batch2_jobs = Vec::new(); + for _ in 0..2 { + let id = Uuid::new_v4(); + insert_flow_job(&db, id, "test-workspace", "f/test/flow").await; + batch2_jobs.push(id); + } + for &j in &batch2_jobs { + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + j, + &args, + &db, + ) + .await?; + } + + let batch1_id: i64 = sqlx::query_scalar!( + "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1", + batch1_jobs[0], + ) + .fetch_one(&db) + .await?; + + // Job 4 (the one that triggered reset) should have a different batch from jobs 1-3 + let reset_batch: i64 = sqlx::query_scalar!( + "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1", + batch1_jobs[3], + ) + .fetch_one(&db) + .await?; + + assert_ne!( + batch1_id, reset_batch, + "reset job should have a different batch ID" + ); + + // Batch 2 jobs should share the same batch but different from batch 1 + let batch2_id: i64 = sqlx::query_scalar!( + "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1", + batch2_jobs[0], + ) + .fetch_one(&db) + .await?; + + assert_ne!( + batch1_id, batch2_id, + "batch 2 should have a different batch ID from batch 1" + ); + + Ok(()) + } + + /// Test: different workspaces with the same debounce_key template produce different + /// resolved keys and do not interfere with each other. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_workspace_isolation(db: Pool) -> anyhow::Result<()> { + // Create a second workspace with required related rows + sqlx::query!( + "INSERT INTO workspace (id, name, owner) VALUES ('ws2', 'Workspace 2', 'test-user')" + ) + .execute(&db) + .await?; + sqlx::query!("INSERT INTO workspace_settings (workspace_id) VALUES ('ws2')") + .execute(&db) + .await?; + + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: None, // default key includes workspace_id + ..Default::default() + }; + let args_hm = empty_args(); + + // Job in workspace 1 + let job_ws1_a = Uuid::new_v4(); + let job_ws1_b = Uuid::new_v4(); + insert_flow_job(&db, job_ws1_a, "test-workspace", "f/test/flow").await; + insert_flow_job(&db, job_ws1_b, "test-workspace", "f/test/flow").await; + + // Job in workspace 2 + let job_ws2 = Uuid::new_v4(); + sqlx::query!( + "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path) + VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', 'ws2', 'f/test/flow')", + job_ws2, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) VALUES ($1, 'ws2', now(), 'flow')", + job_ws2, + ) + .execute(&db) + .await?; + sqlx::query!("INSERT INTO v2_job_runtime (id) VALUES ($1)", job_ws2) + .execute(&db) + .await?; + + // Debounce ws1 job A + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job_ws1_a, + &args, + &db, + ) + .await?; + + // Debounce ws2 job — should NOT debounce ws1 job A + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "ws2", + job_ws2, + &args, + &db, + ) + .await?; + + // ws1 job A should still be queued (not debounced by ws2) + assert!( + is_queued(&db, &job_ws1_a).await, + "ws1 job A should still be queued" + ); + + // Now debounce ws1 job B — should debounce ws1 job A + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job_ws1_b, + &args, + &db, + ) + .await?; + + // ws1 job A should now be completed (debounced by ws1 job B) + assert!( + is_completed(&db, &job_ws1_a).await, + "ws1 job A should be debounced by ws1 job B" + ); + // ws2 job should still be queued + assert!( + is_queued(&db, &job_ws2).await, + "ws2 job should still be queued" + ); + // ws1 job B should still be queued + assert!( + is_queued(&db, &job_ws1_b).await, + "ws1 job B should still be queued" + ); + + Ok(()) + } + + /// Test: debounced job's completed result contains the expected "Debounced by" message. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_completed_result_format( + db: Pool, + ) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_result_format_key".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + + let job1 = Uuid::new_v4(); + let job2 = Uuid::new_v4(); + insert_flow_job(&db, job1, "test-workspace", "f/test/flow").await; + insert_flow_job(&db, job2, "test-workspace", "f/test/flow").await; + + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job1, + &args, + &db, + ) + .await?; + + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job2, + &args, + &db, + ) + .await?; + + // Job 1 should be completed with "Debounced by {job2}" + assert!(is_completed(&db, &job1).await, "job1 should be completed"); + let result: Option = sqlx::query_scalar!( + "SELECT result::text FROM v2_job_completed WHERE id = $1", + job1, + ) + .fetch_one(&db) + .await?; + let result_str = result.expect("result should not be null"); + assert!( + result_str.contains(&format!("Debounced by {job2}")), + "result should contain 'Debounced by {job2}', got: {result_str}" + ); + + Ok(()) + } + + /// Test: debounce logs are appended to both the debounced job and the new job. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_logs_appended(db: Pool) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("pp_logs_test_key".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + + let job1 = Uuid::new_v4(); + let job2 = Uuid::new_v4(); + insert_flow_job(&db, job1, "test-workspace", "f/test/flow").await; + insert_flow_job(&db, job2, "test-workspace", "f/test/flow").await; + + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job1, + &args, + &db, + ) + .await?; + + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job2, + &args, + &db, + ) + .await?; + + // Job 1 (debounced) should have "Debounced by job {job2}" in its logs + let logs1: Option = sqlx::query_scalar!( + r#"SELECT logs as "logs!" FROM job_logs WHERE job_id = $1"#, + job1, + ) + .fetch_optional(&db) + .await?; + let logs1 = logs1.expect("debounced job should have logs"); + assert!( + logs1.contains(&format!("Debounced by job {job2}")), + "debounced job logs should contain 'Debounced by job {job2}', got: {logs1}" + ); + + // Job 2 (new) should have "debounce key" in its logs + let logs2: Option = sqlx::query_scalar!( + r#"SELECT logs as "logs!" FROM job_logs WHERE job_id = $1"#, + job2, + ) + .fetch_optional(&db) + .await?; + let logs2 = logs2.expect("new job should have logs"); + assert!( + logs2.contains("pp_logs_test_key"), + "new job logs should contain the debounce key, got: {logs2}" + ); + + Ok(()) + } + + /// Test: debounce with negative delay behaves like no debounce. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_negative_delay(db: Pool) -> anyhow::Result<()> { + let flow_id = Uuid::new_v4(); + insert_flow_job(&db, flow_id, "test-workspace", "f/test/flow").await; + + let settings = DebouncingSettings { + debounce_delay_s: Some(-5), + debounce_key: Some("pp_negative_delay".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + let args = PushArgs::from(&args_hm); + + let result = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow_id, + &args, + &db, + ) + .await?; + + assert!( + result.is_none(), + "negative delay should be treated as no debounce" + ); + + Ok(()) + } + + /// Test: different runnable_paths with no custom debounce_key produce different resolved keys. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_different_paths_independent( + db: Pool, + ) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: None, // default key includes runnable_path + ..Default::default() + }; + let args_hm = empty_args(); + + // Two jobs on different paths + let job_a = Uuid::new_v4(); + let job_b = Uuid::new_v4(); + let job_a2 = Uuid::new_v4(); + insert_flow_job(&db, job_a, "test-workspace", "f/test/flow_a").await; + insert_flow_job(&db, job_b, "test-workspace", "f/test/flow_b").await; + insert_flow_job(&db, job_a2, "test-workspace", "f/test/flow_a").await; + + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow_a".to_string()), + "test-workspace", + job_a, + &args, + &db, + ) + .await?; + + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow_b".to_string()), + "test-workspace", + job_b, + &args, + &db, + ) + .await?; + + // job_a should still be queued (flow_b shouldn't debounce it) + assert!(is_queued(&db, &job_a).await, "job_a should still be queued"); + + // Now push job_a2 on the same path as job_a — should debounce job_a + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow_a".to_string()), + "test-workspace", + job_a2, + &args, + &db, + ) + .await?; + + assert!( + is_completed(&db, &job_a).await, + "job_a should be debounced by job_a2" + ); + assert!(is_queued(&db, &job_b).await, "job_b should be unaffected"); + assert!(is_queued(&db, &job_a2).await, "job_a2 should be queued"); + + Ok(()) + } + + /// Test: push-time debounce with custom key containing $args interpolation + /// differentiates on arg values. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_push_args_interpolation_differentiates(db: Pool) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("user:$args[user_id]".to_string()), + ..Default::default() + }; + + // Job with user_id = "alice" + let job_alice1 = Uuid::new_v4(); + insert_noop_job(&db, job_alice1, "test-workspace").await; + let mut hm = HashMap::new(); + hm.insert( + "user_id".to_string(), + RawValue::from_string("\"alice\"".to_string()).unwrap(), + ); + let args = PushArgs::from(&hm); + let mut sf = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut sf, + &None, + "test-workspace", + JobKind::Noop, + job_alice1, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + + // Job with user_id = "bob" + let job_bob = Uuid::new_v4(); + insert_noop_job(&db, job_bob, "test-workspace").await; + let mut hm = HashMap::new(); + hm.insert( + "user_id".to_string(), + RawValue::from_string("\"bob\"".to_string()).unwrap(), + ); + let args = PushArgs::from(&hm); + let mut sf = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut sf, + &None, + "test-workspace", + JobKind::Noop, + job_bob, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + + // Both should still be queued (different user_id → different keys) + assert!( + is_queued(&db, &job_alice1).await, + "alice job should still be queued" + ); + assert!( + is_queued(&db, &job_bob).await, + "bob job should still be queued" + ); + + // Another alice job should debounce the first + let job_alice2 = Uuid::new_v4(); + insert_noop_job(&db, job_alice2, "test-workspace").await; + let mut hm = HashMap::new(); + hm.insert( + "user_id".to_string(), + RawValue::from_string("\"alice\"".to_string()).unwrap(), + ); + let args = PushArgs::from(&hm); + let mut sf = None; + let mut tx = db.begin().await?; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut sf, + &None, + "test-workspace", + JobKind::Noop, + job_alice2, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + + assert!( + is_completed(&db, &job_alice1).await, + "alice job 1 should be debounced by alice job 2" + ); + assert!( + is_queued(&db, &job_bob).await, + "bob job should be unaffected" + ); + + Ok(()) + } + + /// Test: debounce_key entry points to the latest job after a chain, and + /// previous_job_id tracks the one that was just debounced. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_debounce_key_tracking_chain(db: Pool) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: Some("tracking_chain_key".to_string()), + ..Default::default() + }; + let args_hm = empty_args(); + + let job1 = Uuid::new_v4(); + let job2 = Uuid::new_v4(); + let job3 = Uuid::new_v4(); + insert_flow_job(&db, job1, "test-workspace", "f/test/flow").await; + insert_flow_job(&db, job2, "test-workspace", "f/test/flow").await; + insert_flow_job(&db, job3, "test-workspace", "f/test/flow").await; + + // After job 1 + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job1, + &args, + &db, + ) + .await?; + let dk = get_debounce_key(&db, "tracking_chain_key").await.unwrap(); + assert_eq!(dk.0, job1, "should point to job1"); + assert_eq!(dk.1, None, "no previous job for first entry"); + assert_eq!(dk.2, 0, "debounced_times should be 0"); + + // After job 2 + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job2, + &args, + &db, + ) + .await?; + let dk = get_debounce_key(&db, "tracking_chain_key").await.unwrap(); + assert_eq!(dk.0, job2, "should point to job2"); + assert_eq!(dk.1, Some(job1), "previous should be job1"); + assert_eq!(dk.2, 1, "debounced_times should be 1"); + + // After job 3 + let args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job3, + &args, + &db, + ) + .await?; + let dk = get_debounce_key(&db, "tracking_chain_key").await.unwrap(); + assert_eq!(dk.0, job3, "should point to job3"); + assert_eq!(dk.1, Some(job2), "previous should be job2"); + assert_eq!(dk.2, 2, "debounced_times should be 2"); + + Ok(()) + } + + // ========================================================================= + // Stress test for DB contention (run manually with --ignored) + // ========================================================================= + + /// Stress test: 20,000 debounce operations across 100 keys (200 jobs per key), + /// with bounded concurrency (64 in-flight at a time, matching a large worker fleet). + /// Measures wall-clock time, per-operation latency percentiles, and throughput. + /// + /// Run with: + /// cargo test -p windmill-queue --test debounce_test --features private,enterprise \ + /// -- --ignored test_debounce_contention_stress --nocapture + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + #[ignore] + async fn test_debounce_contention_stress(db: Pool) -> anyhow::Result<()> { + use std::sync::Arc; + use tokio::sync::Semaphore; + + let num_keys: usize = 100; + let jobs_per_key: usize = 200; + let total = num_keys * jobs_per_key; + let max_concurrent: usize = 64; + + // Batch-insert all flow jobs upfront + let all_ids: Vec = (0..total).map(|_| Uuid::new_v4()).collect(); + for chunk in all_ids.chunks(500) { + let chunk_vec: Vec = chunk.to_vec(); + sqlx::query!( + "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path) + SELECT unnest($1::uuid[]), 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace', 'f/test/flow'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) + SELECT unnest($1::uuid[]), 'test-workspace', now(), 'flow'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", + &chunk_vec, + ) + .execute(&db) + .await?; + } + + eprintln!("=== DEBOUNCE CONTENTION STRESS TEST ==="); + eprintln!(" keys: {num_keys}"); + eprintln!(" jobs per key: {jobs_per_key}"); + eprintln!(" total jobs: {total}"); + eprintln!(" max concurrent: {max_concurrent}"); + + let semaphore = Arc::new(Semaphore::new(max_concurrent)); + let start = std::time::Instant::now(); + + let mut handles = Vec::with_capacity(total); + for (i, &flow_id) in all_ids.iter().enumerate() { + let db = db.clone(); + let sem = semaphore.clone(); + let key_index = i % num_keys; + let handle = tokio::spawn(async move { + let _permit = sem.acquire().await.unwrap(); + let settings = DebouncingSettings { + debounce_delay_s: Some(60), + debounce_key: Some(format!("stress_key_{key_index}")), + ..Default::default() + }; + let args_hm: HashMap> = HashMap::new(); + let args = PushArgs::from(&args_hm); + + let op_start = std::time::Instant::now(); + let result = windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + flow_id, + &args, + &db, + ) + .await; + let op_duration = op_start.elapsed(); + + (result, op_duration) + }); + handles.push(handle); + } + + let mut error_count = 0; + let mut op_durations = Vec::with_capacity(total); + for handle in handles { + let (result, duration) = handle.await?; + op_durations.push(duration); + if let Err(e) = result { + eprintln!(" error: {e:#}"); + error_count += 1; + } + } + + let wall_time = start.elapsed(); + + // Compute stats + op_durations.sort(); + let p50 = op_durations[total / 2]; + let p95 = op_durations[total * 95 / 100]; + let p99 = op_durations[total * 99 / 100]; + let max = op_durations[total - 1]; + let ops_per_sec = total as f64 / wall_time.as_secs_f64(); + + // Each key group should have exactly 1 survivor in queue + let queued_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM v2_job_queue WHERE id = ANY($1)", + &all_ids, + ) + .fetch_one(&db) + .await?; + + let completed_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM v2_job_completed WHERE id = ANY($1)", + &all_ids, + ) + .fetch_one(&db) + .await?; + + eprintln!(" wall time: {wall_time:?}"); + eprintln!(" ops/sec: {ops_per_sec:.0}"); + eprintln!(" p50 latency: {p50:?}"); + eprintln!(" p95 latency: {p95:?}"); + eprintln!(" p99 latency: {p99:?}"); + eprintln!(" max latency: {max:?}"); + eprintln!(" errors: {error_count}"); + eprintln!(" queued: {queued_count} (expected {num_keys})"); + eprintln!( + " completed: {completed_count} (expected {})", + total - num_keys + ); + eprintln!("======================================="); + + assert_eq!(error_count, 0, "no errors expected, got {error_count}"); + assert_eq!( + queued_count, num_keys as i64, + "expected {num_keys} survivors (1 per key), got {queued_count}" + ); + assert_eq!( + completed_count, + (total - num_keys) as i64, + "expected {} debounced, got {completed_count}", + total - num_keys + ); + + Ok(()) + } + + /// Stress test for push-time maybe_debounce: concurrent operations across multiple keys, + /// each holding a caller transaction open (simulating push_inner) while debouncing. + /// + /// Note: push-time debounce holds a caller tx AND `add_completed_job` needs its own + /// pool connection, so each concurrent push needs 2 pool connections. The sqlx::test + /// pool defaults to ~10 connections, so max_concurrent must be <= pool_size/2. + /// In production, pool_size ~50 allows ~25 concurrent pushes per server. + /// + /// Run with: + /// cargo test -p windmill-queue --test debounce_test --features private,enterprise \ + /// -- --ignored test_push_debounce_contention_stress --nocapture + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + #[ignore] + async fn test_push_debounce_contention_stress(db: Pool) -> anyhow::Result<()> { + use std::sync::Arc; + use tokio::sync::Semaphore; + + let num_keys: usize = 10; + let jobs_per_key: usize = 100; + let total = num_keys * jobs_per_key; + // Each push holds 1 tx + add_completed_job needs 1 more = 2 connections. + // sqlx::test pool is ~10, so max_concurrent = 4 to stay safe. + let max_concurrent: usize = 4; + + // Batch-insert all jobs upfront + let all_ids: Vec = (0..total).map(|_| Uuid::new_v4()).collect(); + for chunk in all_ids.chunks(500) { + let chunk_vec: Vec = chunk.to_vec(); + sqlx::query!( + "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id) + SELECT unnest($1::uuid[]), 'noop', 'deno', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) + SELECT unnest($1::uuid[]), 'test-workspace', now(), 'deno'", + &chunk_vec, + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", + &chunk_vec, + ) + .execute(&db) + .await?; + } + + eprintln!("=== PUSH-TIME DEBOUNCE CONTENTION STRESS TEST ==="); + eprintln!(" keys: {num_keys}"); + eprintln!(" jobs per key: {jobs_per_key}"); + eprintln!(" total jobs: {total}"); + eprintln!(" max concurrent: {max_concurrent}"); + + let semaphore = Arc::new(Semaphore::new(max_concurrent)); + let start = std::time::Instant::now(); + + let mut handles = Vec::with_capacity(total); + for (i, &job_id) in all_ids.iter().enumerate() { + let db = db.clone(); + let sem = semaphore.clone(); + let key_index = i % num_keys; + let handle = tokio::spawn(async move { + let _permit = sem.acquire().await.unwrap(); + let settings = DebouncingSettings { + debounce_delay_s: Some(60), + debounce_key: Some(format!("push_stress_key_{key_index}")), + ..Default::default() + }; + let args_hm: HashMap> = HashMap::new(); + let args = PushArgs::from(&args_hm); + + let op_start = std::time::Instant::now(); + + // Simulate push_inner: open a caller tx, call maybe_debounce, + // then commit (mirroring the real push flow). + let mut tx = db.begin().await?; + let mut scheduled_for = None; + windmill_queue::jobs_ee::maybe_debounce( + &settings, + &mut scheduled_for, + &None, + "test-workspace", + JobKind::Script, + job_id, + &args, + &mut tx, + ) + .await?; + tx.commit().await?; + + let op_duration = op_start.elapsed(); + Ok::<_, windmill_common::error::Error>((scheduled_for, op_duration)) + }); + handles.push(handle); + } + + let mut error_count = 0; + let mut op_durations = Vec::with_capacity(total); + for handle in handles { + match handle.await? { + Ok((_scheduled_for, duration)) => { + op_durations.push(duration); + } + Err(e) => { + eprintln!(" error: {e:#}"); + error_count += 1; + op_durations.push(std::time::Duration::ZERO); + } + } + } + + let wall_time = start.elapsed(); + + // Compute stats + op_durations.sort(); + let p50 = op_durations[total / 2]; + let p95 = op_durations[total * 95 / 100]; + let p99 = op_durations[total * 99 / 100]; + let max = op_durations[total - 1]; + let ops_per_sec = total as f64 / wall_time.as_secs_f64(); + + let queued_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM v2_job_queue WHERE id = ANY($1)", + &all_ids, + ) + .fetch_one(&db) + .await?; + + let completed_count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM v2_job_completed WHERE id = ANY($1)", + &all_ids, + ) + .fetch_one(&db) + .await?; + + eprintln!(" wall time: {wall_time:?}"); + eprintln!(" ops/sec: {ops_per_sec:.0}"); + eprintln!(" p50 latency: {p50:?}"); + eprintln!(" p95 latency: {p95:?}"); + eprintln!(" p99 latency: {p99:?}"); + eprintln!(" max latency: {max:?}"); + eprintln!(" errors: {error_count}"); + eprintln!(" queued: {queued_count} (expected {num_keys})"); + eprintln!( + " completed: {completed_count} (expected {})", + total - num_keys + ); + eprintln!("================================================="); + + assert_eq!(error_count, 0, "no errors expected, got {error_count}"); + assert_eq!( + queued_count, num_keys as i64, + "expected {num_keys} survivors (1 per key), got {queued_count}" + ); + assert_eq!( + completed_count, + (total - num_keys) as i64, + "expected {} debounced, got {completed_count}", + total - num_keys + ); + + Ok(()) + } + + /// Helper: insert a flow job with args into v2_job + v2_job_queue + v2_job_runtime. + async fn insert_flow_job_with_args( + db: &Pool, + job_id: Uuid, + workspace_id: &str, + runnable_path: &str, + args: &serde_json::Value, + ) { + sqlx::query!( + "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path, args) + VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', $2, $3, $4)", + job_id, + workspace_id, + runnable_path, + args, + ) + .execute(db) + .await + .expect("insert v2_job with args"); + + sqlx::query!( + "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) + VALUES ($1, $2, now(), 'flow')", + job_id, + workspace_id, + ) + .execute(db) + .await + .expect("insert v2_job_queue"); + + sqlx::query!("INSERT INTO v2_job_runtime (id) VALUES ($1)", job_id) + .execute(db) + .await + .expect("insert v2_job_runtime"); + } + + /// Test: debounce_args_to_accumulate excludes the named arg from the debounce key, + /// so jobs with different values for that arg still debounce each other. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_args_to_accumulate_same_key( + db: Pool, + ) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: None, // default key (includes args minus accumulated ones) + debounce_args_to_accumulate: Some(vec!["items".to_string()]), + ..Default::default() + }; + + // Job 1: items = ["a", "b"] + let job1 = Uuid::new_v4(); + let args1 = serde_json::json!({"items": ["a", "b"], "other": "same"}); + insert_flow_job_with_args(&db, job1, "test-workspace", "f/test/flow", &args1).await; + + // Job 2: items = ["c", "d"] (different items, same "other") + let job2 = Uuid::new_v4(); + let args2 = serde_json::json!({"items": ["c", "d"], "other": "same"}); + insert_flow_job_with_args(&db, job2, "test-workspace", "f/test/flow", &args2).await; + + let args_hm1: HashMap> = serde_json::from_value(args1).unwrap(); + let args = PushArgs::from(&args_hm1); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job1, + &args, + &db, + ) + .await?; + + let args_hm2: HashMap> = serde_json::from_value(args2).unwrap(); + let args = PushArgs::from(&args_hm2); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job2, + &args, + &db, + ) + .await?; + + // Job 1 should be debounced (completed) because "items" is excluded from key + assert!( + is_completed(&db, &job1).await, + "job1 should be debounced despite different 'items' values" + ); + assert!( + is_queued(&db, &job2).await, + "job2 should still be queued (survivor)" + ); + + Ok(()) + } + + /// Test: debounce_args_to_accumulate does NOT cause debouncing when non-accumulated + /// args differ — only the accumulated arg is excluded from the key. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_args_to_accumulate_different_non_accumulated( + db: Pool, + ) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: None, + debounce_args_to_accumulate: Some(vec!["items".to_string()]), + ..Default::default() + }; + + // Job 1: other = "foo" + let job1 = Uuid::new_v4(); + let args1 = serde_json::json!({"items": ["a"], "other": "foo"}); + insert_flow_job_with_args(&db, job1, "test-workspace", "f/test/flow", &args1).await; + + // Job 2: other = "bar" (different non-accumulated arg) + let job2 = Uuid::new_v4(); + let args2 = serde_json::json!({"items": ["b"], "other": "bar"}); + insert_flow_job_with_args(&db, job2, "test-workspace", "f/test/flow", &args2).await; + + let args_hm1: HashMap> = serde_json::from_value(args1).unwrap(); + let args = PushArgs::from(&args_hm1); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job1, + &args, + &db, + ) + .await?; + + let args_hm2: HashMap> = serde_json::from_value(args2).unwrap(); + let args = PushArgs::from(&args_hm2); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + job2, + &args, + &db, + ) + .await?; + + // Both should still be queued — different "other" arg means different keys + assert!( + is_queued(&db, &job1).await, + "job1 should still be queued (different key due to 'other' arg)" + ); + assert!( + is_queued(&db, &job2).await, + "job2 should still be queued (different key due to 'other' arg)" + ); + + Ok(()) + } + + /// Test: batch tracking correctly groups debounced jobs so that accumulated args + /// can be collected at execution time via v2_job_debounce_batch. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_post_preprocessing_args_to_accumulate_batch_collection( + db: Pool, + ) -> anyhow::Result<()> { + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: None, + debounce_args_to_accumulate: Some(vec!["items".to_string()]), + ..Default::default() + }; + + // Create 3 jobs with different "items" but same "other" + let jobs: Vec<(Uuid, serde_json::Value)> = vec![ + ( + Uuid::new_v4(), + serde_json::json!({"items": ["a", "b"], "other": "x"}), + ), + ( + Uuid::new_v4(), + serde_json::json!({"items": ["c"], "other": "x"}), + ), + ( + Uuid::new_v4(), + serde_json::json!({"items": ["d", "e", "f"], "other": "x"}), + ), + ]; + + for (id, args) in &jobs { + insert_flow_job_with_args(&db, *id, "test-workspace", "f/test/flow", args).await; + } + + for (id, args) in &jobs { + let args_hm: HashMap> = + serde_json::from_value(args.clone()).unwrap(); + let push_args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + *id, + &push_args, + &db, + ) + .await?; + } + + let survivor = jobs[2].0; // last job survives + assert!( + is_queued(&db, &survivor).await, + "last job should be the survivor" + ); + + // All 3 jobs should be in the same debounce batch + let batch_ids: Vec = sqlx::query_scalar!( + "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = ANY($1)", + &jobs.iter().map(|(id, _)| *id).collect::>(), + ) + .fetch_all(&db) + .await?; + + assert_eq!(batch_ids.len(), 3, "all 3 jobs should have batch entries"); + assert!( + batch_ids.iter().all(|b| *b == batch_ids[0]), + "all jobs should share the same batch ID" + ); + + // Simulate what maybe_apply_debouncing does: collect accumulated args from batch + let accumulated: Vec> = sqlx::query_scalar!( + "WITH ids AS ( + SELECT id as job_id FROM v2_job_debounce_batch WHERE debounce_batch = ( + SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1 + ) + ) SELECT args->>'items' FROM ids LEFT JOIN v2_job ON v2_job.id = ids.job_id", + survivor, + ) + .fetch_all(&db) + .await?; + + // Merge all items arrays (same logic as maybe_apply_debouncing) + let mut all_items: Vec = vec![]; + for s in accumulated.iter().flatten() { + let items: Vec = serde_json::from_str(s).unwrap(); + all_items.extend(items); + } + all_items.sort_by(|a, b| a.as_str().unwrap().cmp(b.as_str().unwrap())); + + assert_eq!( + all_items, + vec!["a", "b", "c", "d", "e", "f"], + "accumulated items should contain all items from all debounced jobs" + ); + + Ok(()) + } + + /// Test: maybe_apply_debouncing actually merges accumulated args into the surviving job's args. + /// This is an end-to-end test that sets up runnable_settings in the DB, constructs a + /// PulledJobResult, and verifies the accumulated arg is written into the job. + #[sqlx::test(migrations = "../migrations", fixtures("base"))] + async fn test_maybe_apply_debouncing_merges_accumulated_args( + db: Pool, + ) -> anyhow::Result<()> { + use windmill_common::runnable_settings::RunnableSettings; + use windmill_common::runnable_settings::{ + insert_rs, ConcurrencySettings, RunnableSettingsTrait, + }; + use windmill_queue::{MiniPulledJob, PulledJob, PulledJobResult}; + + let settings = DebouncingSettings { + debounce_delay_s: Some(5), + debounce_key: None, + debounce_args_to_accumulate: Some(vec!["items".to_string()]), + ..Default::default() + }; + + // Insert debouncing_settings and concurrency_settings into the DB + let debouncing_hash = settings.insert_cached(&db).await?; + let concurrency_hash = ConcurrencySettings::default().insert_cached(&db).await?; + + let rs = RunnableSettings { + debouncing_settings: debouncing_hash, + concurrency_settings: concurrency_hash, + }; + let rs_handle = insert_rs(rs, &db).await?; + + // Create 3 jobs with different "items" values + let jobs: Vec<(Uuid, serde_json::Value)> = vec![ + ( + Uuid::new_v4(), + serde_json::json!({"items": [1, 2], "other": "x"}), + ), + ( + Uuid::new_v4(), + serde_json::json!({"items": [3], "other": "x"}), + ), + ( + Uuid::new_v4(), + serde_json::json!({"items": [4, 5, 6], "other": "x"}), + ), + ]; + + for (id, args) in &jobs { + insert_flow_job_with_args(&db, *id, "test-workspace", "f/test/flow", args).await; + // Set runnable_settings_handle on the job + sqlx::query!( + "UPDATE v2_job_queue SET runnable_settings_handle = $1 WHERE id = $2", + rs_handle, + id, + ) + .execute(&db) + .await?; + } + + // Debounce all 3 jobs via post-preprocessing + for (id, args) in &jobs { + let args_hm: HashMap> = + serde_json::from_value(args.clone()).unwrap(); + let push_args = PushArgs::from(&args_hm); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &settings, + &Some("f/test/flow".to_string()), + "test-workspace", + *id, + &push_args, + &db, + ) + .await?; + } + + let survivor_id = jobs[2].0; + assert!( + is_queued(&db, &survivor_id).await, + "last job should survive" + ); + + // Build a PulledJobResult for the surviving job (mimicking what the worker does) + let survivor_args: HashMap> = + serde_json::from_value(jobs[2].1.clone()).unwrap(); + + let mini = MiniPulledJob { + workspace_id: "test-workspace".to_string(), + id: survivor_id, + args: Some(sqlx::types::Json(survivor_args)), + parent_job: None, + created_by: "test-user".to_string(), + scheduled_for: Utc::now(), + started_at: None, + runnable_path: Some("f/test/flow".to_string()), + kind: JobKind::Flow, + runnable_id: None, + canceled_reason: None, + canceled_by: None, + permissioned_as: "u/test-user".to_string(), + permissioned_as_email: "test@windmill.dev".to_string(), + flow_status: None, + tag: "flow".to_string(), + script_lang: None, + same_worker: false, + pre_run_error: None, + concurrent_limit: None, + concurrency_time_window_s: None, + flow_innermost_root_job: None, + root_job: None, + timeout: None, + flow_step_id: None, + cache_ttl: None, + cache_ignore_s3_path: None, + priority: None, + preprocessed: None, + script_entrypoint_override: None, + trigger: None, + trigger_kind: None, + visible_to_owner: false, + permissioned_as_end_user_email: None, + runnable_settings_handle: rs_handle, + }; + + let pulled = PulledJob { + job: mini, + raw_code: None, + raw_lock: None, + raw_flow: None, + parent_runnable_path: None, + permissioned_as_email: None, + permissioned_as_username: None, + permissioned_as_is_admin: None, + permissioned_as_is_operator: None, + permissioned_as_groups: None, + permissioned_as_folders: None, + }; + + let mut result = PulledJobResult { + job: Some(pulled), + suspended: false, + missing_concurrency_key: false, + error_while_preprocessing: None, + }; + + // Call the real maybe_apply_debouncing + result.maybe_apply_debouncing(&db).await?; + + // The job should still be present (not debounced itself) + assert!( + result.job.is_some(), + "survivor job should not be nulled out" + ); + + let job = result.job.unwrap(); + let args = job.job.args.expect("args should be present"); + let items_raw = args.get("items").expect("items arg should exist"); + let items: Vec = serde_json::from_str(items_raw.get())?; + + // Should have all 6 items accumulated from all 3 debounced jobs + let mut item_nums: Vec = items + .iter() + .map(|v| v.as_i64().expect("item should be a number")) + .collect(); + item_nums.sort(); + + assert_eq!( + item_nums, + vec![1, 2, 3, 4, 5, 6], + "accumulated items should contain all values from all debounced jobs" + ); + + // "other" arg should be unchanged + let other_raw = args.get("other").expect("other arg should exist"); + let other: String = serde_json::from_str(other_raw.get())?; + assert_eq!(other, "x", "non-accumulated arg should be unchanged"); + + Ok(()) + } +} diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 7b013bda9e..bc630b3190 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -1328,29 +1328,30 @@ pub async fn update_flow_status_after_job_completion_internal( if module_step.is_preprocessor_step() && success { let tag_and_concurrency_key = get_tag_and_concurrency(&flow, db).await; - let require_args = tag_and_concurrency_key.as_ref().is_some_and(|x| { + let has_debouncing = flow_value + .debouncing_settings + .debounce_delay_s + .filter(|x| *x > 0) + .is_some(); + let concurrency_requires_args = tag_and_concurrency_key.as_ref().is_some_and(|x| { x.tag.as_ref().is_some_and(|t| t.contains("$args")) || x.concurrency_key .as_ref() .is_some_and(|ck| ck.contains("$args")) }); - let mut tag = tag_and_concurrency_key - .as_ref() - .map(|x| x.tag.clone()) - .flatten(); + let require_args = concurrency_requires_args || has_debouncing; + let mut tag = tag_and_concurrency_key.as_ref().and_then(|x| x.tag.clone()); let concurrency_key = tag_and_concurrency_key .as_ref() - .map(|x| x.concurrency_key.clone()) - .flatten(); + .and_then(|x| x.concurrency_key.clone()); let concurrent_limit = tag_and_concurrency_key .as_ref() - .map(|x| x.concurrent_limit) - .flatten(); + .and_then(|x| x.concurrent_limit); let concurrency_time_window_s = tag_and_concurrency_key .as_ref() - .map(|x| x.concurrency_time_window_s) - .flatten(); - if require_args { + .and_then(|x| x.concurrency_time_window_s); + + let fetched_args = if require_args { let args = sqlx::query_scalar!( "SELECT result as \"result: Json>>\" FROM v2_job_completed @@ -1362,8 +1363,13 @@ pub async fn update_flow_status_after_job_completion_internal( .map_err(|e| { Error::internal_err(format!("error while fetching preprocessing args: {e:#}")) })?; - let args_hm = args.unwrap_or_default().0; - let args = PushArgs::from(&args_hm); + Some(args.unwrap_or_default().0) + } else { + None + }; + + if concurrency_requires_args { + let args = PushArgs::from(fetched_args.as_ref().unwrap()); if let Some(ck) = concurrency_key { insert_concurrency_key( &flow_job.workspace_id, @@ -1392,8 +1398,31 @@ pub async fn update_flow_status_after_job_completion_internal( .await?; } - // let tag = tag_and_concurrency_key.and_then(|tc| tc.tag.map(|t| interpolate_args(t.clone(), &args, &workspace_id))); - // let concurrency_key = tag_and_concurrency_key.and_then(|tc| tc.concurrency_key.map(|ck| interpolate_args(&ck, &args, &workspace_id))); + let scheduled_for: Option> = { + #[cfg(feature = "private")] + { + if has_debouncing { + let empty_hm = HashMap::new(); + let args = PushArgs::from(fetched_args.as_ref().unwrap_or(&empty_hm)); + windmill_queue::jobs_ee::maybe_debounce_post_preprocessing( + &flow_value.debouncing_settings, + &flow_job.runnable_path, + &flow_job.workspace_id, + flow, + &args, + db, + ) + .await? + } else { + None + } + } + #[cfg(not(feature = "private"))] + { + None + } + }; + sqlx::query!( "WITH job_result AS ( SELECT result @@ -1403,7 +1432,8 @@ pub async fn update_flow_status_after_job_completion_internal( updated_queue AS ( UPDATE v2_job_queue SET running = false, - tag = COALESCE($3, tag) + tag = COALESCE($3, tag), + scheduled_for = COALESCE($6, scheduled_for) WHERE id = $2 ) UPDATE v2_job @@ -1431,6 +1461,7 @@ pub async fn update_flow_status_after_job_completion_internal( tag, concurrent_limit, concurrency_time_window_s, + scheduled_for, ) .execute(db) .await From b60f309a0cc45f531f4fc3166a9e01b5faccc528 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 23 Feb 2026 08:46:35 +0100 Subject: [PATCH 13/16] chore(main): release 1.642.0 (#8046) * chore(main): release 1.642.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> --- CHANGELOG.md | 13 ++ backend/Cargo.lock | 140 +++++++++--------- backend/Cargo.toml | 4 +- backend/windmill-api/openapi.yaml | 2 +- benchmarks/lib.ts | 2 +- cli/src/main.ts | 2 +- frontend/package-lock.json | 50 ++++++- frontend/package.json | 2 +- lsp/Pipfile | 4 +- openflow.openapi.yaml | 2 +- .../WindmillClient/WindmillClient.psd1 | 2 +- python-client/wmill/pyproject.toml | 2 +- python-client/wmill_pg/pyproject.toml | 2 +- typescript-client/jsr.json | 2 +- typescript-client/package.json | 2 +- version.txt | 2 +- 16 files changed, 145 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dff6dec2c..bb041998cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [1.642.0](https://github.com/windmill-labs/windmill/compare/v1.641.0...v1.642.0) (2026-02-22) + + +### Features + +* **cli:** add consistent get/list/new subcommands for all item types ([#8047](https://github.com/windmill-labs/windmill/issues/8047)) ([4fedfdf](https://github.com/windmill-labs/windmill/commit/4fedfdfd11aa8ca7fff6f7aed5ae2b313888f878)) + + +### Bug Fixes + +* make WM_FLOW_PATH available in flow step previews ([#8042](https://github.com/windmill-labs/windmill/issues/8042)) ([a91c532](https://github.com/windmill-labs/windmill/commit/a91c532ecadce63cea965c497351fa1a6f39697a)) +* preserve debouncing settings for flows with preprocessors ([#8043](https://github.com/windmill-labs/windmill/issues/8043)) ([a00927b](https://github.com/windmill-labs/windmill/commit/a00927b3008a2d953fde1d461723a3c92f375eb4)) + ## [1.641.0](https://github.com/windmill-labs/windmill/compare/v1.640.0...v1.641.0) (2026-02-21) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index d355cc7f64..c54583f985 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -15725,7 +15725,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-nats", @@ -15789,7 +15789,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -15802,7 +15802,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "argon2", @@ -15940,7 +15940,7 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -15963,7 +15963,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -15976,7 +15976,7 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16002,7 +16002,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.641.0" +version = "1.642.0" dependencies = [ "reqwest 0.12.28", "serde", @@ -16012,7 +16012,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16029,7 +16029,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "base64 0.22.1", @@ -16052,7 +16052,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16075,7 +16075,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16091,7 +16091,7 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16111,7 +16111,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16131,7 +16131,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16145,7 +16145,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-nats", @@ -16171,7 +16171,7 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16196,7 +16196,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "flate2", @@ -16213,7 +16213,7 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16234,7 +16234,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16254,7 +16254,7 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16284,7 +16284,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16311,7 +16311,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.641.0" +version = "1.642.0" dependencies = [ "lazy_static", "serde", @@ -16323,7 +16323,7 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.641.0" +version = "1.642.0" dependencies = [ "argon2", "axum 0.7.9", @@ -16346,7 +16346,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16360,7 +16360,7 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.641.0" +version = "1.642.0" dependencies = [ "axum 0.7.9", "chrono", @@ -16390,7 +16390,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.641.0" +version = "1.642.0" dependencies = [ "chrono", "lazy_static", @@ -16404,7 +16404,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -16423,7 +16423,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.641.0" +version = "1.642.0" dependencies = [ "aes-gcm", "anyhow", @@ -16522,7 +16522,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.641.0" +version = "1.642.0" dependencies = [ "chrono", "itertools 0.14.0", @@ -16541,7 +16541,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.641.0" +version = "1.642.0" dependencies = [ "regex", "serde", @@ -16556,7 +16556,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "astral-tokio-tar", @@ -16580,7 +16580,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "futures", @@ -16597,7 +16597,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.641.0" +version = "1.642.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -16613,7 +16613,7 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-trait", @@ -16634,7 +16634,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-trait", @@ -16665,7 +16665,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-oauth2", @@ -16689,7 +16689,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-stream", @@ -16723,7 +16723,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "futures", @@ -16741,7 +16741,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.641.0" +version = "1.642.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -16750,7 +16750,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "lazy_static", @@ -16762,7 +16762,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "serde_json", @@ -16774,7 +16774,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "gosyn", @@ -16786,7 +16786,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "lazy_static", @@ -16798,7 +16798,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "serde_json", @@ -16810,7 +16810,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "nu-parser", @@ -16821,7 +16821,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -16832,7 +16832,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -16845,7 +16845,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-recursion", @@ -16869,7 +16869,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "lazy_static", @@ -16883,7 +16883,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -16900,7 +16900,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "lazy_static", @@ -16915,7 +16915,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "lazy_static", @@ -16934,7 +16934,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "serde", @@ -16945,7 +16945,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-recursion", @@ -16982,7 +16982,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "const_format", @@ -17020,7 +17020,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.641.0" +version = "1.642.0" dependencies = [ "wasm-bindgen", "wasm-bindgen-test", @@ -17030,7 +17030,7 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-recursion", @@ -17059,7 +17059,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -17082,7 +17082,7 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-trait", @@ -17115,7 +17115,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-trait", @@ -17135,7 +17135,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-trait", @@ -17169,7 +17169,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-trait", @@ -17204,7 +17204,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-trait", @@ -17227,7 +17227,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-trait", @@ -17251,7 +17251,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-nats", @@ -17275,7 +17275,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-trait", @@ -17310,7 +17310,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-trait", @@ -17338,7 +17338,7 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-trait", @@ -17361,7 +17361,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "bitflags 2.9.4", @@ -17379,7 +17379,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.641.0" +version = "1.642.0" dependencies = [ "anyhow", "async-once-cell", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 74163ddf40..29cc8aa28c 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.641.0" +version = "1.642.0" authors.workspace = true edition.workspace = true @@ -76,7 +76,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal"] [workspace.package] -version = "1.641.0" +version = "1.642.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index f68c8f2354..27e535f8bf 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.641.0 + version: 1.642.0 title: Windmill API contact: diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 77f0603a00..138edfcbe0 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.641.0"; +export const VERSION = "v1.642.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/main.ts b/cli/src/main.ts index cfd366081a..730e0574d2 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -65,7 +65,7 @@ export { workspaceAdd, }; -export const VERSION = "1.641.0"; +export const VERSION = "1.642.0"; // Re-exported from constants.ts to maintain backwards compatibility export { WM_FORK_PREFIX } from "./core/constants.ts"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ca78b11190..4aac239774 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.641.0", + "version": "1.642.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.641.0", + "version": "1.642.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -835,6 +835,7 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -846,6 +847,7 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -856,6 +858,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1345,6 +1348,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", "integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1499,6 +1503,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1515,6 +1520,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1531,6 +1537,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1547,6 +1554,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1563,6 +1571,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1579,6 +1588,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1595,6 +1605,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1611,6 +1622,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1627,6 +1639,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1643,6 +1656,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1659,6 +1673,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1675,6 +1690,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1691,6 +1707,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2296,6 +2313,7 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7175,7 +7193,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7674,6 +7692,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7694,6 +7713,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7714,6 +7734,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7734,6 +7755,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7754,6 +7776,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7774,6 +7797,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7794,6 +7818,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7814,6 +7839,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7834,6 +7860,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7854,6 +7881,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7874,6 +7902,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12500,6 +12529,21 @@ } } }, + "node_modules/svelte-check/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 16b0eed47b..c4cc0b9426 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.641.0", + "version": "1.642.0", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/lsp/Pipfile b/lsp/Pipfile index 6518a6608b..962386beb7 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,8 +4,8 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.641.0" -wmill_pg = ">=1.641.0" +wmill = ">=1.642.0" +wmill_pg = ">=1.642.0" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 3f60492a46..612151d752 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: '3.0.3' info: - version: 1.641.0 + version: 1.642.0 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index 392eef58ec..5e49354d9a 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.641.0' + ModuleVersion = '1.642.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index 9c223ebe00..a0292fa174 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.641.0" +version = "1.642.0" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/python-client/wmill_pg/pyproject.toml b/python-client/wmill_pg/pyproject.toml index 67474d7df8..1fd178b845 100644 --- a/python-client/wmill_pg/pyproject.toml +++ b/python-client/wmill_pg/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill-pg" -version = "1.641.0" +version = "1.642.0" description = "An extension client for the wmill client library focused on pg" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index 12994d9884..65b3c47927 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.641.0", + "version": "1.642.0", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index 924432dceb..73a87dc17d 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.641.0", + "version": "1.642.0", "author": "Ruben Fiszel", "license": "Apache 2.0", "sideEffects": false, diff --git a/version.txt b/version.txt index 113adc7af4..4be1988217 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.641.0 +1.642.0 From f0b7c96d04a9d71f969e9fa930f427053f40b46e Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 23 Feb 2026 09:09:16 +0000 Subject: [PATCH 14/16] cli zsh completions nit --- cli/src/main.ts | 17 +++++++++++++++-- cli/test_completions4.ts | 13 +++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 cli/test_completions4.ts diff --git a/cli/src/main.ts b/cli/src/main.ts index 730e0574d2..64ceb67b2d 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -1,5 +1,5 @@ import { Command } from "@cliffy/command"; -import { CompletionsCommand } from "@cliffy/command/completions"; +import { generateShellCompletions } from "@cliffy/command/completions"; import { UpgradeCommand } from "@cliffy/command/upgrade"; import * as log from "./core/log.ts"; @@ -172,7 +172,20 @@ const command = new Command() ); }) ) - .command("completions", new CompletionsCommand()); + .command( + "completions", + new Command() + .description("Generate shell completions.") + .command("bash", new Command().description("Generate bash completions.").action(() => { + process.stdout.write(generateShellCompletions(command, "bash") + "\n"); + })) + .command("zsh", new Command().description("Generate zsh completions.").action(() => { + process.stdout.write(generateShellCompletions(command, "zsh") + "\n"); + })) + .command("fish", new Command().description("Generate fish completions.").action(() => { + process.stdout.write(generateShellCompletions(command, "fish") + "\n"); + })) + ); async function main() { try { diff --git a/cli/test_completions4.ts b/cli/test_completions4.ts new file mode 100644 index 0000000000..77f60959f2 --- /dev/null +++ b/cli/test_completions4.ts @@ -0,0 +1,13 @@ +import { Command } from "@cliffy/command"; +import { ZshCompletionsGenerator } from "@cliffy/command/completions/_zsh_completions_generator"; +const { default: command } = await import("./src/main.ts"); + +// Generate completions manually +const output = ZshCompletionsGenerator.generate("wmill", command); +console.error(`output length: ${output.length}`); + +// Write using process.stdout.write with callback +process.stdout.write(output + "\n", () => { + console.error("write callback called"); + process.stdin.destroy(); +}); From 9686608355615a50c8395f6e2fd51dcc25498226 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Mon, 23 Feb 2026 10:38:22 +0100 Subject: [PATCH 15/16] fix(backend): decimal between 0 and -1 in mssql (#8051) --- backend/windmill-worker/src/mssql_executor.rs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/backend/windmill-worker/src/mssql_executor.rs b/backend/windmill-worker/src/mssql_executor.rs index 3a62481935..c1f8ae2fe7 100644 --- a/backend/windmill-worker/src/mssql_executor.rs +++ b/backend/windmill-worker/src/mssql_executor.rs @@ -11,7 +11,6 @@ use tiberius::{ use tokio::net::TcpStream; use tokio_util::compat::TokioAsyncWriteCompatExt; use uuid::Uuid; -use windmill_object_store::convert_json_line_stream; use windmill_common::utils::merge_raw_values_to_object; use windmill_common::worker::SqlResultCollectionStrategy; use windmill_common::{ @@ -19,6 +18,7 @@ use windmill_common::{ utils::empty_as_none, worker::{to_raw_value, Connection}, }; +use windmill_object_store::convert_json_line_stream; use windmill_parser_sql::{parse_db_resource, parse_mssql_sig, parse_s3_mode}; use windmill_queue::MiniPulledJob; use windmill_queue::{append_logs, CanceledBy}; @@ -428,9 +428,7 @@ fn sql_to_json_value(val: ColumnData) -> Result, Error> { } fn numeric_to_raw_value(numeric: &tiberius::numeric::Numeric) -> Result, Error> { - // tiberius::Numeric::to_string is broken, don't use it - - let sign = if numeric.int_part().is_negative() { + let sign = if numeric.value().is_negative() { "-" } else { "" @@ -468,6 +466,7 @@ where #[cfg(test)] mod tests { use super::*; + use tiberius::numeric::Numeric; #[test] fn test_sql_to_json_value_numeric_null() { @@ -477,7 +476,6 @@ mod tests { #[test] fn test_sql_to_json_value_numeric_integer() { - use tiberius::numeric::Numeric; let numeric = Numeric::new_with_scale(12345, 0); let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap(); assert_eq!(result.get(), "12345"); @@ -485,7 +483,6 @@ mod tests { #[test] fn test_sql_to_json_value_numeric_decimal() { - use tiberius::numeric::Numeric; let numeric = Numeric::new_with_scale(123456, 2); // Represents 1234.56 let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap(); assert_eq!(result.get(), "1234.56"); @@ -493,7 +490,6 @@ mod tests { #[test] fn test_sql_to_json_value_numeric_negative() { - use tiberius::numeric::Numeric; let numeric = Numeric::new_with_scale(-98765, 2); // Represents -987.65 let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap(); assert_eq!(result.get(), "-987.65"); @@ -501,7 +497,6 @@ mod tests { #[test] fn test_sql_to_json_value_numeric_negative_integer() { - use tiberius::numeric::Numeric; let numeric = Numeric::new_with_scale(-98765, 0); let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap(); assert_eq!(result.get(), "-98765"); @@ -509,15 +504,21 @@ mod tests { #[test] fn test_sql_to_json_value_numeric_high_precision() { - use tiberius::numeric::Numeric; let numeric = Numeric::new_with_scale(123456789012345, 10); // High precision let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap(); assert_eq!(result.get(), "12345.6789012345"); } + #[test] + fn test_sql_to_json_value_numeric_negative_fractional_only() { + // -0.4: int_part() is 0, so old code lost the negative sign + let numeric = Numeric::new_with_scale(-4, 1); + let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap(); + assert_eq!(result.get(), "-0.4"); + } + #[test] fn test_sql_to_json_value_numeric_7_69() { - use tiberius::numeric::Numeric; let numeric = Numeric::new_with_scale(769, 2); let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap(); assert_eq!(result.get(), "7.69"); From 0aa885db67d77202205fc1609e841b8ffd9a8121 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Mon, 23 Feb 2026 12:05:29 +0100 Subject: [PATCH 16/16] fix(backend): use filename instead of content_type to detect file fields in multipart form data (#8054) String fields with an explicit Content-Type (e.g. text/plain) were incorrectly treated as file uploads and sent to S3. Per RFC 7578, the presence of a filename parameter is what distinguishes file fields from regular form fields. Co-authored-by: Claude Opus 4.6 --- backend/windmill-api/src/args.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/windmill-api/src/args.rs b/backend/windmill-api/src/args.rs index c71fff0acb..2734be6740 100644 --- a/backend/windmill-api/src/args.rs +++ b/backend/windmill-api/src/args.rs @@ -91,8 +91,8 @@ impl RawWebhookArgs { get_random_file_name, get_workspace_s3_resource, upload_file_internal, }; use futures::TryStreamExt; - use windmill_object_store::object_store_reexports::{Attribute, Attributes}; use windmill_object_store::build_object_store_client; + use windmill_object_store::object_store_reexports::{Attribute, Attributes}; let (_, s3_resource) = get_workspace_s3_resource(authed, db, None, w_id, None).await?; @@ -106,20 +106,24 @@ impl RawWebhookArgs { Error::BadRequest(format!("Error reading multipart field: {}", e.body_text())) })? { if let Some(name) = field.name().map(|x| x.to_string()) { - if let Some(content_type) = field.content_type() { + if field.file_name().is_some() { + let content_type = field + .content_type() + .unwrap_or("application/octet-stream") + .to_string(); let ext = field .file_name() - .map(|x| x.split('.').last()) - .flatten() + .and_then(|x| x.split('.').last()) .map(|x| x.to_string()); + let filename = field.file_name().map(|x| x.to_string()); let file_key = get_random_file_name(ext); let options = Attributes::from_iter(vec![ - (Attribute::ContentType, content_type.to_string()), + (Attribute::ContentType, content_type), ( Attribute::ContentDisposition, - if let Some(filename) = field.file_name() { + if let Some(filename) = filename { format!("inline; filename=\"{}\"", filename) } else { "inline".to_string()