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