Compare commits

..

95 Commits

Author SHA1 Message Date
centdix
36aadbebec Merge branch 'main' into frontdev 2026-02-23 13:29:18 +00:00
hugocasa
0aa885db67 fix(backend): use filename instead of content_type to detect file fields in multipart form data (#8054)
String fields with an explicit Content-Type (e.g. text/plain) were
incorrectly treated as file uploads and sent to S3. Per RFC 7578, the
presence of a filename parameter is what distinguishes file fields from
regular form fields.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:05:29 +00:00
hugocasa
9686608355 fix(backend): decimal between 0 and -1 in mssql (#8051) 2026-02-23 09:38:22 +00:00
Ruben Fiszel
f0b7c96d04 cli zsh completions nit 2026-02-23 09:09:16 +00:00
Ruben Fiszel
b60f309a0c chore(main): release 1.642.0 (#8046)
* chore(main): release 1.642.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-23 07:46:35 +00:00
centdix
bedba3b75e fix: move pane zoom into attach cmd chain for reliable initial zoom
- Pass initialPane to attach() so zoom runs inside the shell command
  chain where tmux is guaranteed to exist (no external race)
- Send initialPane in the first resize WS message (atomic, single msg)
- Remove pendingPane from WsData (dead code from iterative patching)
- Fix unzoom: use shell conditional instead of broken tmux if-shell

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 00:41:23 +00:00
centdix
771d67c849 fix: pane zoom race condition and desktop unzoom on attach
- Bun processes async WS messages sequentially, so selectPane always
  arrives after attach completes — call selectPane() directly when
  attached instead of only queueing
- Always unzoom on attach via tmux if-shell #{window_zoomed_flag} so
  desktop never starts with a pane zoomed from a previous mobile session
- Remove unreliable setTimeout approach, send selectPane from client
  immediately after resize

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 00:27:14 +00:00
centdix
46d486960a feat: make dev-dashboard mobile-friendly
Collapsible sidebar (full-screen overlay on mobile), hamburger menu,
bottom pane switcher bar for full (Claude/Backend/Frontend) and sandbox
(Claude/Shell) profiles, auto-zoom into Claude pane on mobile connect,
larger terminal font on mobile, and iOS overscroll prevention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 00:04:03 +00:00
centdix
ca73267cbb complete readme 2026-02-22 23:56:12 +00:00
centdix
6eabb8db63 nit 2026-02-22 16:59:53 +00:00
centdix
5cc8f20cd2 docs: add Cursor IDE new window tip to dev-dashboard README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 16:00:07 +00:00
centdix
ea5312e940 fix: prevent stale proc exit from deleting active terminal session
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:44:27 +00:00
centdix
c62ba73ce4 fix: select claude pane on attach and restart tmux if dead
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:41:22 +00:00
Ruben Fiszel
a00927b300 fix: preserve debouncing settings for flows with preprocessors (#8043)
* fix: preserve debouncing settings for flows with preprocessors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revert "fix: preserve debouncing settings for flows with preprocessors"

This reverts commit 3452c1657c.

* feat: add post-preprocessing debounce for flows with preprocessors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* perf: reuse caller tx for push-time debounce and add stress test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add exhaustive edge case tests for debouncing behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* perf: optimize debouncing to reduce DB round-trips

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: replace legacy debounce compat with error logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add debounce args accumulation tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add end-to-end test for maybe_apply_debouncing arg accumulation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: update sqlx offline query cache

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: make workmux pane commands idempotent for replay

Use git rev-parse --show-toplevel to resolve absolute paths instead of
relative cd, so commands work when replayed from within backend/frontend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add e2e debounce test script for backend API

Comprehensive end-to-end test covering:
- Deploy & run scripts rapidly (no debounce with different args)
- Redeploy without lock in rapid succession
- Debounce with same args (should consolidate)
- Debounce with different args (should not consolidate)
- Custom debounce key behavior
- Git sync debounce + item aggregation (using glob-style ** path filter)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref to 0fede4b1086bc1456be9cc55b203228c979c5c5e

This commit updates the EE repository reference after PR #426 was merged in windmill-ee-private.

Previous ee-repo-ref: b5d333370603a6cc7ef70842354cf3be734241b4

New ee-repo-ref: 0fede4b1086bc1456be9cc55b203228c979c5c5e

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-02-22 14:16:52 +00:00
centdix
98ac164ac8 merge 2026-02-22 14:11:44 +00:00
centdix
ed6aaeeea3 fix: use WM_WORKTREE_PATH in worktree-cleanup and add debug logs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:07:47 +00:00
centdix
5e415c0a12 playwirght 2026-02-22 14:03:49 +00:00
centdix
35dded1347 feat: sanitize worktree name input to valid git branch name
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-22 13:51:26 +00:00
centdix
aedf012c84 fix: restore dialog focus using programmatic focus instead of autofocus
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 13:51:19 +00:00
centdix
62fea97547 feat: add Cmd+M keyboard shortcut for merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 13:48:35 +00:00
centdix
96c2d88d91 feat: add merge endpoint and button, remove unused close/send endpoints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 13:46:59 +00:00
centdix
5eb308ad35 nit 2026-02-22 13:42:55 +00:00
centdix
cfa04e1188 kill process on clenaup 2026-02-22 13:42:46 +00:00
centdix
0d520f730b 0.0.0.0 2026-02-22 13:19:52 +00:00
Ruben Fiszel
3c89c28e71 chore: fix flaky agent token test by not splitting on underscore (#8048)
Base64url encoding uses '_' as a valid character, so splitting the JWT
token on '_' would intermittently break the JWT parsing when the encoded
payload or signature contained underscores. Strip the known prefix instead.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 09:20:55 +01:00
Ruben Fiszel
4fedfdfd11 feat(cli): add consistent get/list/new subcommands for all item types (#8047)
* feat(cli): add consistent get/list/new subcommands for all item types

Make the CLI consistent so every item type (script, flow, app, resource,
resource-type, variable, schedule, folder, trigger) supports get/list/new
subcommands, enabling the CLI to be used as a full API client in bash
scripts with jq piping.

- Add --json flag to all list commands for machine-readable output
- Register explicit "list" subcommand alongside default action
- Add "get <path> [--json]" subcommand to fetch single items from API
- Rename "bootstrap" to "new" for script/flow, keep "bootstrap" as alias
- Add "new" subcommand for resource, resource-type, variable, schedule,
  folder, and trigger to create local template YAML files
- Update cli-commands skill documentation for wmill init
- Add integration tests for all new commands

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* feat: install wmill CLI in Docker images and use it for bash variable/resource access

- Install windmill-cli via bun in all Dockerfiles that include bun
- DockerfileCli: switch from node:slim to oven/bun:slim
- CLI: auto-configure from WM_WORKSPACE/WM_TOKEN/BASE_INTERNAL_URL env vars
  as last-resort fallback when no workspace is configured
- Frontend: replace curl-based bash snippets with wmill variable/resource get
- Add backend integration tests for wmill CLI in bash scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): install windmill-cli in backend test workflow

Ensures wmill is available on PATH for bash integration tests
that use `wmill variable get` and `wmill resource get`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(cli): replace @std/* Deno dependencies with Node.js equivalents

Replace @std/log with a lightweight custom logger (core/log.ts),
@std/path with node:path, and @std/yaml with the yaml npm package.
Also fix process hang on exit, add --node option to install_dev.sh,
and add missing hasRequiredPermissions to NpmProvider.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* all

* all

* refactor(cli): replace @ayonli/jsext and @std/encoding with lightweight alternatives

Replace @ayonli/jsext (8.4MB) with tar-stream (32kB) for tar creation,
replace @std/encoding with Node.js Buffer.toString("hex"), and fix
@windmill-labs/shared-utils to use direct npm instead of JSR mirror.
Also resolve merge conflicts in sync.ts and fix pre-existing type errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): use singleQuote YAML output and pass yamlOptions in gitsync pull

The yaml library defaults to double quotes, but the codebase (and tests)
expect single-quoted strings. Add singleQuote: true to yamlOptions and
pass yamlOptions to gitsync-settings pull writeFile calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* all

* fix(cli): address code review feedback

- Install CLI from source in backend tests instead of npm
- Fix script bootstrap catch block to re-throw "File already exists"
- Add type-safe local variable after trigger kind validation
- Use created_by instead of policy.on_behalf_of for app get output
- Note --kind is recommended for faster trigger lookup in help text
- Document node symlink purpose in Dockerfiles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): use /usr/bin for wmill wrapper to ensure it's in PATH

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): install wmill to ~/.local/bin to avoid permission issues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci(backend): switch to Blacksmith runner and add cargo caching

- Switch from ubicloud-standard-16 to blacksmith-16vcpu-ubuntu-2404 for faster NVMe-backed builds
- Add stickydisk for cargo target directory (persistent NVMe cache across runs)
- Add cache for cargo registry and git dependencies
- Upgrade DuckDB FFI cache from actions/cache@v3 to useblacksmith/cache@v1
- Enable CARGO_INCREMENTAL=1 to benefit from persistent target cache

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix ci

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 07:53:28 +00:00
centdix
8cdc7d6e9e nit 2026-02-22 01:55:41 +00:00
centdix
51077245c7 feat: move SSH settings to gear icon next to Cursor button
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-22 01:50:47 +00:00
centdix
9e387c3559 fix: make playwright browsers dir writable for sandbox user
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 01:48:03 +00:00
centdix
7aea965803 style: reduce terminal font size to 11
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-22 01:46:59 +00:00
centdix
8446e3b551 refactor: overhaul sandbox Dockerfile and fix screenshot instructions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 01:41:04 +00:00
centdix
a6d6136d57 fix: increase Bun.serve idleTimeout to prevent worktree removal timeout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-22 01:40:17 +00:00
centdix
c93b2e287c feat: add screenshot capture and R2 upload support for sandbox agents
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:36:17 +00:00
Ruben Fiszel
a91c532eca fix: make WM_FLOW_PATH available in flow step previews (#8042)
* fix: pass flow path in flow step preview for AI agent modules

JobLoader.runFlowPreview was missing the path parameter, causing
WM_FLOW_PATH to be unavailable when using the Run button on individual
flow steps. Test up to here worked correctly because it uses a
different code path (utils.svelte.ts) that already passed the path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: make WM_FLOW_PATH available for rawscript/script step previews

Inject the flow path as `_flow_path` in the job args when running a
script preview from the flow editor. The SQL pull queries now use
COALESCE to fall back to this arg when no parent runnable path exists,
making WM_FLOW_PATH available for individual step "Run" previews.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: rename _flow_path args key to _FLOW_PATH

Match existing convention used by _ENTRYPOINT_OVERRIDE.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:56:32 +00:00
Ruben Fiszel
18b3c1ae5c nit install dev 2026-02-21 21:44:41 +00:00
centdix
93f927c1c1 fix: add EE repo mount for sandbox and remove agent key listener
Add ~/windmill-ee-private mount to sandbox extra_mounts (needed for
.git access) and remove ArrowLeft/ArrowRight agent-switching keydown
handler from CreateWorktreeDialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:35:53 +00:00
centdix
83f21510f3 feat: add open in Cursor button with SSH remote support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:19:55 +00:00
centdix
5c1f69ddcd nit 2026-02-21 21:19:52 +00:00
Ruben Fiszel
a2cefdf0a2 refactor(cli): migrate CLI from Deno to Bun/Node.js (#8041)
* fix: only enable EE features in test backend when license key is available

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: skip EE tests without license key and exclude test-skills from test discovery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: unskip passing tests and add duplicate (remote, workspaceId) check in addWorkspace

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(cli): migrate from Deno APIs to Node.js/Bun-compatible APIs

Replace Deno-specific APIs with Node.js equivalents across the entire CLI
codebase to enable running on Node.js/Bun. Switch build system from dnt
to bun, update imports from jsr:/npm: prefixed to bare specifiers, and
add package.json/tsconfig.json for the Node.js ecosystem.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* test(cli): expand test coverage with new integration and unit tests

Add standalone_commands.test.ts covering folder list, schedule list,
resource-type list/push/update, script show/run/bootstrap, and user
commands. Add unit tests for filePathExtensionFromContentType and
removeExtensionToPath. Add git_unit, local_encryption_unit,
resource_folders_unit, and settings_unit test files. Fix schedule
cron expressions (6-field format), add includeSchedules flag, improve
test setup with pre-build and auto-cleanup, and support TEST_CLI_RUNTIME=node.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): replace Deno.readFile with node:fs in WASM loaders and add schema parsing tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(cli): switch WASM parsers from local files to npm packages

Use published windmill-parser-wasm-* npm packages instead of local
wasm/ files. A loadParser() helper uses createRequire to resolve the
.wasm binary from node_modules and passes it to init() via
readFileSync, avoiding fetch() and Deno.readFile() patches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(cli): add coverage for --locks-required lint feature

Add 15 tests covering the lock-checking functionality merged from main:
- checkMissingLocks: standalone scripts (python, bun, bash), inline
  lock file resolution (valid, empty, missing), flow inline rawscripts
  (with/without locks, nested forloopflow), app inline scripts, raw
  apps without backend folder
- runLint --locks-required integration: reports issues when locks
  missing, skips checks when flag absent, passes when locks exist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci(cli): replace Deno with Bun in CI workflows

- cli-tests.yml: remove Deno setup, use `bun test` instead of
  `deno test`, add `bun install` step for dependency installation
- npm_on_release.yml: replace Deno setup with Bun setup for CLI
  publishing
- build.sh: add `bun install` before building so CI has dependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): pre-start backend in test preload and remove Deno test leftovers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): normalize path separators for Windows compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* more tests + windows

* ci(cli): use Blacksmith runner for Windows tests

Switch test-windows job from windows-latest to blacksmith-16vcpu-windows-2025
for faster CI execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): fix Windows path separator expectations in unit tests

buildMetadataPath and extractResourceName normalize to forward slashes
internally, so tests should not expect platform-specific separators in
their output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): fix Windows CI test failures for dev_server and script_run

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): set BUN_PATH and NODE_BIN_PATH for backend worker on Windows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci(cli): add SSH debug step on Windows test failure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): use native path separators for ignore check in dev mode on Windows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:19:04 +00:00
centdix
3f2bd424c7 docs: add EE sandbox extra_mounts setup instructions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 21:14:53 +00:00
centdix
0d5f42e89e refactor: replace symlink hooks with additionalDirectories for EE access
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 21:14:53 +00:00
centdix
dfad07881d rm 2026-02-21 21:14:47 +00:00
Ruben Fiszel
c4de11a406 chore(main): release 1.641.0 (#8040)
* chore(main): release 1.641.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-21 22:04:35 +01:00
Ruben Fiszel
fd5ebc2fda fix: tag bunnative dependency jobs as bun instead of nativets (#8045)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 20:59:48 +00:00
centdix
7b6ba7093a feat: remove agent-only profile, add dialog shortcuts, auto-focus terminal
- Remove agent-only profile (only full and agent-yolo remain)
- Arrow up/down in create dialog cycles profiles, left/right cycles agents
- Auto-focus terminal when switching worktrees

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:43:57 +00:00
centdix
8042e33c38 nit 2026-02-21 20:38:44 +00:00
centdix
57d23c92c5 feat: show port status in worktree sidebar list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:34:24 +00:00
centdix
f21140f7cd feat: add run.sh, agent name in sidebar, and vite preview proxy
- run.sh builds frontend then serves in production mode
- Persist AGENT in .env.local and show it in worktree list
- Add preview proxy config so production mode routes API/WS correctly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:34:02 +00:00
centdix
2800226bd4 nit 2026-02-21 20:22:46 +00:00
centdix
2ae82796fc feat: show worktree profile tag in sidebar
Persist profile to .env.local on worktree creation and display it
(full, agent-only, agent-yolo) in the worktree list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:22:33 +00:00
centdix
d1290ba777 feat: add dev.sh script and document keyboard shortcuts
Single script to start both backend and frontend with prefixed logs.
Updated README with dev.sh usage and keyboard shortcut reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:03:40 +00:00
centdix
9de0060884 feat: add keyboard shortcuts for worktree navigation in dev-dashboard
Cmd+Up/Down to switch worktrees, Cmd+K to create new, Cmd+D to remove.
xterm.js passthrough ensures shortcuts bubble up from terminal. Shortcut
hints displayed in a fixed panel at the bottom of the sidebar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:01:45 +00:00
centdix
3df8964fc6 feat: add tmux pane layout to agent system prompt in workmux config
Agent now knows pane 1 is backend (cargo) and pane 2 is frontend (npm),
so "check backend logs" maps directly to the right capture-pane command.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:51:37 +00:00
centdix
fb0b2234ba docs: add dev-dashboard README with architecture and quick start
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:36:03 +00:00
centdix
4064ec3a3d nit 2026-02-21 19:28:31 +00:00
centdix
0db6cbd10c feat: add codex agent support to sandbox container
- Install @openai/codex in Dockerfile.sandbox
- Pass developer_instructions via -c flag with proper shell escaping
- Use --yolo flag for sandbox profile (container is the sandbox)
- Mount ~/.codex into container via workmux extra_mounts config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:27:30 +00:00
centdix
ab91f78017 nit 2026-02-21 19:12:57 +00:00
centdix
1e0245ca9a feat: add codex as agent choice in worktree creation dialog
Add agent selector (Claude/Codex) to the create worktree dialog,
orthogonal to the profile choice. Selection is persisted alongside
the profile default.

Backend builds the appropriate command per agent: codex uses
--full-auto for sandbox, claude uses --dangerously-skip-permissions
with --append-system-prompt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:11:26 +00:00
centdix
bead746bb8 fix: socat zombie reaping, reconcile filter, and polling logs
- Consume proc.exited promise to prevent zombie socat processes
- Use container name prefix filter instead of ancestor (matches
  containers from older image builds)
- Improve polling logs to show retry count and waiting state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:06:48 +00:00
centdix
268b5ee2a8 feat: add save as default checkbox for worktree profile selection
Persist the selected profile to localStorage when checked, and
preselect it on subsequent dialog opens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:04:58 +00:00
centdix
24f2571b37 fix: use PORT env var and cargo watch for backend in system prompt
Change backend start command to use PORT= instead of --port flag and
cargo watch for auto-reload. Install cargo-watch in sandbox container.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 18:59:11 +00:00
centdix
ed1a655317 fix: make cargo registry world-writable in sandbox container
The sqlx-cli install populates /opt/cargo/registry as root. Add
chmod -R a+rwX after the install so the sandbox user can write
to the registry when building.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 18:50:02 +00:00
centdix
a4b440a20d feat: add socat port forwarding for sandbox containers
When a worktree runs inside a Docker sandbox, its ports are only
reachable via the container's bridge IP. socat forwards host ports to
the container so the browser (over SSH) can reach them.

- New socat.ts module manages forwarding lifecycle (start/stop/reconcile)
- Polls for container after creation (non-blocking, up to 30s)
- Kills orphaned socat on startup before re-establishing forwards
- Cleans up on worktree removal and SIGINT/SIGTERM
- Extract readEnvLocal to env.ts to break circular import
- Change isPortListening to HTTP fetch (avoids socat false positives)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 18:49:59 +00:00
centdix
b550be8711 fix: install Node 22 and bootstrap frontend in sandbox container
Replace Debian's Node 18 with NodeSource Node 22. Run npm install and
generate-backend-client in the entrypoint so the frontend is ready.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 18:42:21 +00:00
centdix
071129f03b feat: show clickable port badges in top bar with live status
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 18:02:40 +00:00
centdix
e9ac1ce9eb feat: add logging for worktree lifecycle and port assignments
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 17:53:54 +00:00
centdix
587142ddac fix: assign worktree port slots by scanning existing .env.local files
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 17:53:49 +00:00
centdix
d9ed0c318f fix: run sqlx migrations on sandbox startup and fix cargo permissions
The sandbox entrypoint now runs `sqlx migrate run` after creating the
database so that sqlx compile-time query checks work immediately. Also
makes /opt/cargo world-writable so arbitrary-UID sandbox users can write
to the cargo git cache and registry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 17:37:39 +00:00
centdix
4959a0553a feat: append environment-aware system prompt to claude in worktree agents
Read .env.local from the worktree to get port assignments and build a
system prompt informing Claude of backend/frontend ports and startup
commands. For sandbox profiles, double-escape quotes to survive the
extra shell layer inside the container.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 17:36:06 +00:00
centdix
c4323e40c1 fix: enable clipboard support and suppress browser context menu in web terminal
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 17:22:43 +00:00
centdix
696b8de1ed fix: assign worktree ports by index instead of scanning for free ports
Determine the slot from the worktree's position in workmux list rather
than probing ports. Keeps the port-in-use safety check. Removes unused
find_port helper.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:45:24 +00:00
centdix
5f6dda9060 feat: add shell pane alongside agent in agent-only/yolo profiles
Split a 33% width shell pane on the right, using the worktree
directory from pane 0 so it starts in the correct path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:34:02 +00:00
centdix
8490f4435d feat: extract CreateWorktreeDialog component and fix Enter to confirm
Extract inline create dialog into its own component. Wrap both dialogs
in forms so Enter submits, and autofocus the confirm button in
ConfirmDialog so Enter triggers confirm instead of cancel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:27:43 +00:00
centdix
fa2f65e512 feat: add optional name input to worktree creation dialog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:23:55 +00:00
centdix
b9dec43d2a feat: add postgres + sqlx-cli to sandbox container
Start PostgreSQL (owned by postgres user) in entrypoint.sh with a unix
socket in /tmp so the agent can use DATABASE_URL=postgres:///windmill?host=/tmp
for sqlx migrations and cargo check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:04:23 +00:00
centdix
5227b76c2f fix: run agent-yolo profile inside sandbox container
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:56:34 +00:00
centdix
1643d654a8 feat: add custom sandbox Dockerfile with sudo support
- Add Dockerfile.sandbox with sudo, writable passwd/shadow, and
  entrypoint that registers dynamic UIDs for full root access inside container
- Remove playwright MCP server (npx not available in sandbox)
- Move sandbox host_commands/image config to global workmux config
- Remove git from host_commands to prevent infinite fork bomb via shims

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:55:59 +00:00
Ruben Fiszel
0d3f956e74 workmux nits 2026-02-21 12:07:22 +00:00
Ruben Fiszel
b330f38889 fix: run substitute_ee_code.sh after creating EE worktree
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 11:37:33 +00:00
Ruben Fiszel
9eb15312f6 feat: add .npmrc support for private npm registries (#8039)
* feat: add .npmrc support for private npm registries

Add a new `npmrc` instance setting that accepts full .npmrc file content
for configuring private npm registries. Works with bun (native .npmrc
support since 1.1.18), deno (native .npmrc support in 2.x), and the npm
proxy (parses default registry + auth token from .npmrc).

Legacy `npm_config_registry` and `bunfig_install_scopes` fields are now
hidden when empty, so new users only see the .npmrc field. Also fixes a
pre-existing race condition where gen_bunfig was called after
start_child_process.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 05:29:36 +00:00
Ruben Fiszel
e8a13edde7 fix: add created_by ownership check to update/delete saved inputs (#8038)
* fix: add created_by ownership check to update/delete saved inputs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 04:50:24 +00:00
centdix
65a8789dfd fix: close confirm dialog immediately and gray out removing worktrees
Dismiss the confirmation dialog as soon as the user confirms instead
of waiting for the API call. Show the item grayed out with
pointer-events disabled while deletion is in progress. Auto-select
the previous (or next) worktree when the selected one is removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:35:07 +00:00
centdix
6299e7a36a fix: ensure tmux server is running on dashboard startup
Start a detached tmux session if none exists, so worktree
operations don't fail when tmux hasn't been started yet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:03:25 +00:00
centdix
bcbfe4659d feat: add remove button to each worktree in sidebar list
Show an × button on hover for non-main worktrees. Replace the
boolean showConfirmRemove with a removeBranch string so the
confirm dialog works from both sidebar and top bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:03:06 +00:00
centdix
b2fac069df feat: enable sandbox mode for agent-yolo profile
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 01:49:44 +00:00
centdix
4ab08cb6a1 feat: add agent-yolo profile and hide worktrees without tmux window
Add "Agent (skip permissions)" profile that runs claude with
--dangerously-skip-permissions. Filter worktrees without a tmux
window from the sidebar list instead of showing a disabled entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 01:40:51 +00:00
centdix
d6d4d85d8f fix: enable tmux mouse mode and update gitignore for split layout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 01:29:39 +00:00
centdix
81d386d365 feat: add profile selector for worktree creation
Support "agent-only" and "full" profiles when creating worktrees.
Agent-only skips default pane commands, kills extra panes, and starts
only claude. Full uses the default workmux pane layout. Profile is
selected via a centered dialog in the frontend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 01:10:09 +00:00
centdix
a1b878842f refactor: simplify session view to remove-only with confirmation modal
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 01:04:01 +00:00
centdix
00c3e9baf0 fix: only connect terminal when worktree has a tmux window
Check mux status before attempting terminal connection. Worktrees
without a tmux window (mux !== "✓") show an informational message
instead of failing with "can't find window".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 00:45:25 +00:00
centdix
118dcb59af fix: defer terminal spawn until client reports actual dimensions
Instead of spawning with hardcoded 120x30 on WebSocket open, wait for
the client's first resize message with real fitted dimensions. Fixes
terminal not taking full width/height since script+pipes PTY can't be
resized after creation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 00:35:17 +00:00
centdix
bd583be239 fix: clean up stale tmux sessions on dashboard startup
Server crashes/restarts left orphaned wm-dash-* grouped tmux sessions,
causing "duplicate session" errors on subsequent connections. Now cleans
up stale sessions on startup and pre-emptively before each attach.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 00:32:18 +00:00
centdix
c1f7cb5d42 fix: use script+pipes for terminal PTY and fix frontend sizing
Bun's terminal option data callback doesn't fire inside Bun.serve
context (Bun 1.3.9 bug). Switch to script(1) for PTY allocation with
piped stdin/stdout. Fix terminal not taking full width/height by adding
min-h-0, min-w-0 and width: 100% for proper flex layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 00:04:50 +00:00
centdix
d502ef5029 refactor: split dev-dashboard into separate backend and svelte frontend
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-20 23:33:16 +00:00
centdix
bc7ca9982b feat: add dev-dashboard — browser-based workmux frontend
Web dashboard (Bun + xterm.js) that wraps workmux CLI commands and
renders tmux windows in embedded browser terminals. Replaces direct
tmux navigation with a sidebar-based UI at localhost:5111.

- Bun HTTP server with REST API for worktree CRUD (add/rm/open/close/send)
- Bun.Terminal PTY API to attach to tmux grouped sessions per worktree
- xterm.js frontend with WebSocket bridge for real-time terminal I/O
- Scrollback buffer for reconnection, ResizeObserver for dynamic fitting
- Add direnv allow to worktree-env post-create hook for nix devshell

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 23:07:32 +00:00
256 changed files with 18840 additions and 7083 deletions

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env bash
# Resolve _ee.rs symlinks to actual files so Claude can read them
# This script runs before each user prompt is processed
set -e
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-/home/farhad/windmill}"
MANIFEST_FILE="$PROJECT_DIR/.claude/hooks/.symlink-manifest"
# Find all _ee.rs symlinks and store their targets
find "$PROJECT_DIR" -name "*_ee.rs" -type l 2>/dev/null | while read -r symlink; do
target=$(readlink -f "$symlink" 2>/dev/null) || continue
# Only process if target file exists
if [[ -f "$target" ]]; then
# Store symlink path and target in manifest
echo "$symlink|$target" >> "$MANIFEST_FILE.tmp"
# Replace symlink with actual file content
rm "$symlink"
cp "$target" "$symlink"
fi
done
# Atomically replace manifest
if [[ -f "$MANIFEST_FILE.tmp" ]]; then
mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE"
fi
exit 0

View File

@@ -1,36 +0,0 @@
#!/usr/bin/env bash
# Restore _ee.rs symlinks after Claude finishes processing
# This script runs when Claude stops
# IMPORTANT: Copies any modifications back to the target before restoring symlinks
set -e
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-/home/farhad/windmill}"
MANIFEST_FILE="$PROJECT_DIR/.claude/hooks/.symlink-manifest"
# Check if manifest exists
if [[ ! -f "$MANIFEST_FILE" ]]; then
exit 0
fi
# Read manifest and restore symlinks
while IFS='|' read -r symlink target; do
if [[ -n "$symlink" && -n "$target" ]]; then
# If the file exists (not a symlink) and target exists, copy changes back
if [[ -f "$symlink" && ! -L "$symlink" && -e "$target" ]]; then
# Copy the potentially modified file back to the target
cp "$symlink" "$target"
fi
# Remove the regular file (which was a copy)
rm -f "$symlink" 2>/dev/null || true
# Recreate the symlink
ln -s "$target" "$symlink" 2>/dev/null || true
fi
done < "$MANIFEST_FILE"
# Clean up manifest
rm -f "$MANIFEST_FILE"
exit 0

View File

@@ -1,5 +1,8 @@
{
"permissions": {
"additionalDirectories": [
"../windmill-ee-private"
],
"allow": [
"Bash(ls:*)",
"Bash(grep:*)",
@@ -63,39 +66,6 @@
},
"enableAllProjectMcpServers": true,
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/resolve-symlinks.sh",
"timeout": 30
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/restore-symlinks.sh",
"timeout": 30
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/restore-symlinks.sh",
"timeout": 30
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",

View File

@@ -44,6 +44,10 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
# Bun
COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun
# Install windmill CLI
RUN bun install -g windmill-cli \
&& ln -s $(bun pm bin -g)/wmill /usr/bin/wmill
ARG TARGETPLATFORM
# Deno

View File

@@ -19,7 +19,7 @@ defaults:
jobs:
cargo_test:
runs-on: ubicloud-standard-16
runs-on: blacksmith-16vcpu-ubuntu-2404
services:
postgres:
image: postgres
@@ -70,6 +70,16 @@ jobs:
with:
ruby-version: "3.3"
bundler-cache: false
- name: Install windmill CLI from source
run: |
cd $GITHUB_WORKSPACE/cli
bash gen_wm_client.sh
bun install
mkdir -p "$HOME/.local/bin"
printf '#!/bin/sh\nexec bun run "%s/cli/src/main.ts" "$@"\n' "$GITHUB_WORKSPACE" > "$HOME/.local/bin/wmill"
chmod +x "$HOME/.local/bin/wmill"
echo "$HOME/.local/bin" >> $GITHUB_PATH
working-directory: /
- name: Install PowerShell, mold and clang
run: |
sudo apt-get update && sudo apt-get install -y powershell mold clang libcurl4-openssl-dev
@@ -78,6 +88,20 @@ jobs:
with:
cache: false
toolchain: 1.93.0
- name: Cache cargo target directory
uses: useblacksmith/stickydisk@v1
with:
key: cargo-target
path: ./backend/target
- name: Cache cargo registry
uses: useblacksmith/cache@v1
with:
path: |
~/.cargo/registry
~/.cargo/git
key: cargo-registry-${{ hashFiles('backend/Cargo.lock') }}
restore-keys: |
cargo-registry-
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV"
@@ -165,6 +189,12 @@ jobs:
fi
echo "NPM_TOKEN=${NPM_TOKEN}" >> $GITHUB_ENV
{
echo "TEST_NPMRC<<NPMRC_EOF"
echo "@windmill-test:registry=http://localhost:4873/"
echo "//localhost:4873/:_authToken=${NPM_TOKEN}"
echo "NPMRC_EOF"
} >> $GITHUB_ENV
echo "Got NPM token successfully: ${NPM_TOKEN:0:10}..."
# Configure npm globally with the auth token
@@ -199,7 +229,7 @@ jobs:
fi
echo "Verified: Package requires authentication for @windmill-test/private-pkg"
- name: Cache DuckDB FFI module build
uses: actions/cache@v3
uses: useblacksmith/cache@v1
with:
path: ./backend/windmill-duckdb-ffi-internal/target
key: ${{ runner.os }}-duckdb-ffi-${{ hashFiles('./backend/windmill-duckdb-ffi-internal/src/**/*.rs', './backend/windmill-duckdb-ffi-internal/Cargo.toml', './backend/windmill-duckdb-ffi-internal/Cargo.lock') }}
@@ -215,6 +245,7 @@ jobs:
RUST_LOG_STYLE: never
CARGO_NET_GIT_FETCH_WITH_CLI: true
CARGO_BUILD_JOBS: 12
CARGO_INCREMENTAL: 1
WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1
WMDEBUG_FORCE_RUNNABLE_SETTINGS_V0: 1
WMDEBUG_FORCE_NO_LEGACY_DEBOUNCING_COMPAT: 1

View File

@@ -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:

View File

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

3
.gitignore vendored
View File

@@ -17,6 +17,9 @@ rust-client/Cargo.toml
# Worktree-generated port isolation
.env.local
# Worktree-specific Claude Code settings (generated by scripts/worktree-env)
.claude/settings.local.json
# Symlinked cache directories (for git worktrees)
backend/target
frontend/node_modules

View File

@@ -3,10 +3,12 @@
"svelte": {
"type": "http",
"url": "https://mcp.svelte.dev/mcp"
},
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
},
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest"
]
}
}

View File

@@ -12,8 +12,24 @@ window_prefix: "wm-"
auto_name:
model: "claude-sonnet-4.6"
system_prompt: "Generate a kebab-case git branch name."
background: true # Always run in background when using --auto-name
system_prompt: |
Generate a concise git branch name based on the task description.
Rules:
- Use kebab-case (lowercase with hyphens)
- Keep it short: 1-3 words, max 4 if necessary
- Focus on the core task/feature, not implementation details
- No prefixes like feat/, fix/, chore/
Examples of good branch names:
- "Add dark mode toggle" → dark-mode
- "Fix the search results not showing" → fix-search
- "Refactor the authentication module" → auth-refactor
- "Add CSV export to reports" → export-csv
- "Shell completion is broken" → shell-completion
Output ONLY the branch name, nothing else.
background: true
# Commands to run in new worktree before tmux window opens.
@@ -30,11 +46,20 @@ pre_remove:
- ./scripts/worktree-cleanup
panes:
- command: <agent>
- command: >-
claude --append-system-prompt
"You are running inside a tmux session with other panes running services.\n
Pane layout (current window):\n
- Pane 0: this pane (claude agent)\n
- Pane 1: backend (cargo watch -x run)\n
- Pane 2: frontend (npm run dev)\n\n
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (backend) or \`tmux capture-pane -t .2 -p -S -50\` (frontend).\n
When restarting backend or frontend, make sure to use the ports listed in .env.local.\n
Because we are running backend with cargo watch, to verify your changes, just check the logs in the backend pane. No need for cargo check."
focus: true
- command: "[ -f .env.local ] && source .env.local; cd backend && PORT=${BACKEND_PORT:-8000} cargo watch -x run"
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x run'
split: horizontal
- command: "[ -f .env.local ] && source .env.local; cd frontend && npm install && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000}"
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/frontend" && npm install && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0'
split: vertical
files:
@@ -45,3 +70,6 @@ files:
sandbox:
enabled: false
toolchain: off
# image, host_commands, and extra_mounts configured in global
# ~/.config/workmux/config.yaml — see README_WORKMUX_DEV.md for required
# extra_mounts (windmill-ee-private access in sandbox)

View File

@@ -1,5 +1,32 @@
# Changelog
## [1.642.0](https://github.com/windmill-labs/windmill/compare/v1.641.0...v1.642.0) (2026-02-22)
### Features
* **cli:** add consistent get/list/new subcommands for all item types ([#8047](https://github.com/windmill-labs/windmill/issues/8047)) ([4fedfdf](https://github.com/windmill-labs/windmill/commit/4fedfdfd11aa8ca7fff6f7aed5ae2b313888f878))
### Bug Fixes
* make WM_FLOW_PATH available in flow step previews ([#8042](https://github.com/windmill-labs/windmill/issues/8042)) ([a91c532](https://github.com/windmill-labs/windmill/commit/a91c532ecadce63cea965c497351fa1a6f39697a))
* preserve debouncing settings for flows with preprocessors ([#8043](https://github.com/windmill-labs/windmill/issues/8043)) ([a00927b](https://github.com/windmill-labs/windmill/commit/a00927b3008a2d953fde1d461723a3c92f375eb4))
## [1.641.0](https://github.com/windmill-labs/windmill/compare/v1.640.0...v1.641.0) (2026-02-21)
### Features
* add .npmrc support for private npm registries ([#8039](https://github.com/windmill-labs/windmill/issues/8039)) ([9eb1531](https://github.com/windmill-labs/windmill/commit/9eb15312f663aa6d700e8ac562d7b5c75c2221f7))
### Bug Fixes
* add created_by ownership check to update/delete saved inputs ([#8038](https://github.com/windmill-labs/windmill/issues/8038)) ([e8a13ed](https://github.com/windmill-labs/windmill/commit/e8a13edde7c0ba2ef80344ab7c7288e7bb2eb6b5))
* run substitute_ee_code.sh after creating EE worktree ([b330f38](https://github.com/windmill-labs/windmill/commit/b330f388894ecd9cc6b64297420ac6f032d32f72))
* tag bunnative dependency jobs as bun instead of nativets ([#8045](https://github.com/windmill-labs/windmill/issues/8045)) ([fd5ebc2](https://github.com/windmill-labs/windmill/commit/fd5ebc2fda589c022074c3bb4dcdb447c7f86cf0))
## [1.640.0](https://github.com/windmill-labs/windmill/compare/v1.639.0...v1.640.0) (2026-02-20)

View File

@@ -258,6 +258,10 @@ COPY --from=denoland/deno:2.2.1 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun
# Install windmill CLI
RUN bun install -g windmill-cli \
&& ln -s $(bun pm bin -g)/wmill /usr/bin/wmill
COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer

234
Dockerfile.sandbox Normal file
View File

@@ -0,0 +1,234 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
git \
iptables \
gosu \
sudo \
unzip \
# Rust native build deps (for cargo check)
pkg-config \
cmake \
clang \
mold \
libtool \
libssl-dev \
libxml2-dev \
libxmlsec1-dev \
libxslt1-dev \
libffi-dev \
zlib1g-dev \
libcurl4-openssl-dev \
libclang-dev \
libkrb5-dev \
libsasl2-dev \
# PostgreSQL (for local DB during development)
postgresql \
postgresql-client \
# Node.js 22 (for npm run check / frontend dev)
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* \
# Container runs as arbitrary UIDs (--user uid:gid). These three lines make
# sudo work for any UID:
# 1) NOPASSWD rule so sudo never prompts for a password
# 2) Writable passwd/group so the entrypoint can register the dynamic UID
# 3) Writable shadow so unix_chkpwd can validate the account (without this,
# sudo fails with "account validation failure, is your account locked?")
&& echo "ALL ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/sandbox \
&& chmod 0440 /etc/sudoers.d/sandbox \
&& chmod 666 /etc/passwd /etc/group /etc/shadow
# ── GitHub CLI (for PR creation) ──────────────────────────────────────────────
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update && apt-get install -y --no-install-recommends gh \
&& rm -rf /var/lib/apt/lists/*
# ── Rust toolchain ────────────────────────────────────────────────────────────
# Install under /usr/local/lib/ so bins are world-readable with default umask.
# CARGO_HOME is overridden to /tmp/.cargo at the end for mutable runtime state.
ENV RUSTUP_HOME=/usr/local/lib/rustup CARGO_HOME=/usr/local/lib/cargo
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain stable --profile minimal && \
ln -s /usr/local/lib/cargo/bin/* /usr/local/bin/
RUN cargo install sqlx-cli --no-default-features --features native-tls,postgres && \
cargo install cargo-watch && \
ln -sf /usr/local/lib/cargo/bin/sqlx /usr/local/bin/sqlx && \
ln -sf /usr/local/lib/cargo/bin/cargo-watch /usr/local/bin/cargo-watch
# ── Register dynamic runtime users ───────────────────────────────────────────
RUN cat <<'SCRIPT' > /usr/local/bin/register-dynamic-user.sh
#!/bin/sh
set -eu
uid="${1:-}"
gid="${2:-}"
if [ -z "$uid" ] || [ -z "$gid" ]; then
echo "register-dynamic-user: usage: register-dynamic-user <uid> <gid>" >&2
exit 1
fi
if ! getent group "$gid" >/dev/null 2>&1; then
echo "sandbox:x:${gid}:" >> /etc/group
fi
if ! getent passwd "$uid" >/dev/null 2>&1; then
echo "sandbox:x:${uid}:${gid}:sandbox:/tmp:/bin/sh" >> /etc/passwd
fi
# Add a shadow entry ("*" = no password) so unix_chkpwd doesn't reject sudo.
if ! grep -q "^sandbox:" /etc/shadow 2>/dev/null; then
echo "sandbox:*:19000:0:99999:7:::" >> /etc/shadow
fi
SCRIPT
RUN chmod +x /usr/local/bin/register-dynamic-user.sh
# ── Network init script (iptables firewall + privilege drop) ──────────────────
RUN cat <<'SCRIPT' > /usr/local/bin/network-init.sh
#!/bin/bash
set -euo pipefail
if [ -n "${WM_PROXY_HOST:-}" ] && [ -n "${WM_PROXY_PORT:-}" ]; then
# Resolve hostnames to ALL IPs (multi-A records, round-robin DNS)
PROXY_IPS=$(getent ahostsv4 "$WM_PROXY_HOST" | awk '{print $1}' | sort -u)
RPC_HOST="${WM_RPC_HOST:-$WM_PROXY_HOST}"
RPC_IPS=$(getent ahostsv4 "$RPC_HOST" | awk '{print $1}' | sort -u)
if [ -z "$PROXY_IPS" ] || [ -z "$RPC_IPS" ]; then
echo "network-init: failed to resolve proxy/RPC host" >&2
exit 1
fi
# IPv4: default deny outbound
iptables -P OUTPUT DROP
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow DNS (UDP/TCP 53) to configured nameservers.
if [ -f /etc/resolv.conf ]; then
grep '^nameserver' /etc/resolv.conf | awk '{print $2}' | while read -r ns; do
iptables -A OUTPUT -d "$ns" -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -d "$ns" -p tcp --dport 53 -j ACCEPT
done
fi
# Allow ALL resolved proxy IPs (handles multi-A DNS)
for ip in $PROXY_IPS; do
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_PROXY_PORT" -j ACCEPT
done
# Allow ALL resolved RPC IPs
if [ -n "${WM_RPC_PORT:-}" ]; then
for ip in $RPC_IPS; do
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_RPC_PORT" -j ACCEPT
done
fi
# Reject (not drop) everything else to fail fast instead of hanging
iptables -A OUTPUT -j REJECT
# IPv6: block entirely to prevent leaks (fail closed)
if ip6tables -L -n >/dev/null 2>&1; then
ip6tables -P OUTPUT DROP
ip6tables -A OUTPUT -o lo -j ACCEPT
ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
ip6tables -A OUTPUT -j REJECT
else
if ! sysctl -w net.ipv6.conf.all.disable_ipv6=1 2>/dev/null; then
echo "network-init: failed to block IPv6 (neither ip6tables nor sysctl available)" >&2
exit 1
fi
fi
fi
# Add sandbox user/group so sudo works after dropping privileges.
if [ -z "${WM_TARGET_UID:-}" ] || [ -z "${WM_TARGET_GID:-}" ]; then
echo "network-init: WM_TARGET_UID and WM_TARGET_GID are required" >&2
exit 1
fi
/usr/local/bin/register-dynamic-user.sh "${WM_TARGET_UID}" "${WM_TARGET_GID}"
# Fix PTY ownership so the unprivileged user can read/write the terminal.
if [ -t 0 ]; then
chown "${WM_TARGET_UID}:${WM_TARGET_GID}" "$(tty)"
fi
# Drop privileges and exec the user command.
exec gosu "${WM_TARGET_UID}:${WM_TARGET_GID}" env HOME=/tmp "$@"
SCRIPT
RUN chmod +x /usr/local/bin/network-init.sh
# ── workmux (sandbox RPC) ────────────────────────────────────────────────────
RUN curl -fsSL https://raw.githubusercontent.com/raine/workmux/main/scripts/install.sh | bash
# ── Claude Code ───────────────────────────────────────────────────────────────
RUN curl -fsSL https://claude.ai/install.sh | bash && \
target="$(readlink -f /root/.local/bin/claude)" && \
mv /root/.local/share/claude /usr/local/lib/claude && \
ln -s "/usr/local/lib/claude/versions/$(basename "$target")" /usr/local/bin/claude && \
mkdir -p /tmp/.local/bin && \
ln -s /usr/local/bin/claude /tmp/.local/bin/claude
# ── Codex ─────────────────────────────────────────────────────────────────────
RUN npm i -g @openai/codex
# ── Bun ───────────────────────────────────────────────────────────────────────
ENV BUN_INSTALL=/usr/local/lib/bun
RUN curl -fsSL https://bun.sh/install | bash && \
ln -s /usr/local/lib/bun/bin/bun /usr/local/bin/bun && \
ln -s /usr/local/lib/bun/bin/bunx /usr/local/bin/bunx
# ── Playwright + Chromium (for screenshots) ──────────────────────────────────
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/lib/playwright-browsers
RUN bun add -g @playwright/test \
&& bunx playwright install chromium --with-deps \
&& chmod -R a+rwX /usr/local/lib/playwright-browsers \
&& rm -rf /var/lib/apt/lists/* /tmp/bunx-*
# ── AWS CLI (for S3-compatible uploads to R2) ─────────────────────────────────
RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip \
&& unzip -q /tmp/awscliv2.zip -d /tmp \
&& /tmp/aws/install \
&& rm -rf /tmp/aws /tmp/awscliv2.zip
ENV AWS_DEFAULT_REGION=auto
# ── Runtime env for arbitrary UID ─────────────────────────────────────────────
# Mutable state goes to /tmp (writable by any UID). Toolchains stay read-only.
ENV CARGO_HOME=/tmp/.cargo BUN_TMPDIR=/tmp
# ── Entrypoint ────────────────────────────────────────────────────────────────
RUN cat <<'ENTRY' > /usr/local/bin/entrypoint.sh
#!/bin/sh
/usr/local/bin/register-dynamic-user.sh "$(id -u)" "$(id -g)"
# Start PostgreSQL (unix socket in /tmp, owned by postgres user)
mkdir -p /tmp/pgdata && sudo chown postgres:postgres /tmp/pgdata
if [ ! -f /tmp/pgdata/PG_VERSION ]; then
sudo -u postgres /usr/lib/postgresql/15/bin/initdb -D /tmp/pgdata --auth=trust
fi
sudo -u postgres /usr/lib/postgresql/15/bin/pg_ctl -D /tmp/pgdata -l /tmp/pg.log start -o "-k /tmp"
sudo -u postgres psql -h /tmp -c "CREATE ROLE sandbox SUPERUSER LOGIN" 2>/dev/null || true
sudo -u postgres createdb -h /tmp windmill 2>/dev/null || true
# Run database migrations so sqlx compile-time checks work
if [ -d "$PWD/backend/migrations" ]; then
DATABASE_URL="postgres://sandbox@localhost/windmill?host=/tmp" \
sqlx migrate run --source "$PWD/backend/migrations" 2>/dev/null || true
fi
# Install frontend dependencies and generate backend client
if [ -d "$PWD/frontend" ]; then
(cd "$PWD/frontend" && npm install && npm run generate-backend-client) 2>/dev/null || true
fi
exec "$@"
ENTRY
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

View File

@@ -172,6 +172,25 @@ The setup is defined in `.workmux.yaml` at the repo root. Key sections:
- **`files.copy`**: Copies `backend/.env` and `scripts/` into each worktree
- **`files.symlink`**: Symlinks `node_modules` and `.svelte-kit` to avoid reinstalling per worktree
## Enterprise (EE) Code Access
The enterprise source code lives in the `windmill-ee-private` repository (sibling to this repo). When you create a worktree, `scripts/worktree-env` automatically creates a matching EE worktree on the same branch and configures Claude Code's `additionalDirectories` to grant access.
### Sandbox setup
When using sandbox mode, the container needs explicit mounts to access the EE repo. Add the following to your global workmux config (`~/.config/workmux/config.yaml`):
```yaml
sandbox:
extra_mounts:
- host_path: ~/windmill-ee-private
writable: true
- host_path: ~/windmill-ee-private__worktrees
writable: true
```
This mounts both the main EE repo (used by the main worktree) and the EE worktrees directory (used by feature worktrees) into every sandbox container.
## Login
Default credentials: `admin@windmill.dev` / `changeme`

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n SELECT unnest($1::uuid[]), 'test-workspace', now(), 'flow'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "0681b850c033619e1b9498376263681f875a5aba22170ca50ec8b578f7fa478b"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH completed AS (\n INSERT INTO v2_job_completed\n (workspace_id, id, started_at, duration_ms, result,\n flow_status, workflow_as_code_status, status, worker)\n SELECT\n q.workspace_id, q.id, q.started_at,\n (EXTRACT('epoch' FROM now()) - EXTRACT('epoch' FROM COALESCE(q.started_at, now()))) * 1000,\n CASE WHEN q.running\n THEN $3::text::jsonb\n ELSE $4::text::jsonb\n END,\n s.flow_status,\n s.workflow_as_code_status,\n 'skipped'::job_status,\n q.worker\n FROM v2_job_queue q\n LEFT JOIN v2_job_status s ON s.id = q.id\n WHERE q.id = $1\n ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, result = EXCLUDED.result\n RETURNING 1 AS x\n ), _deleted AS (\n DELETE FROM v2_job_queue WHERE id = $1\n ), _logged AS (\n INSERT INTO job_logs (logs, job_id, workspace_id)\n VALUES ($5, $1, $2)\n ON CONFLICT (job_id) DO UPDATE SET logs = concat(job_logs.logs, EXCLUDED.logs)\n )\n SELECT x FROM completed\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "x",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Text",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "1437b432d2c23e30eb05443e83069cdb049f65ec299b0778ce14677728cf6346"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT result::text FROM v2_job_completed WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "18b6262a60400f2b58ab26615466c23b4c1a7805c66b70b0fcfb7d33b122a7bf"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n VALUES ($1, $2, now(), 'flow')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": []
},
"hash": "1af6885dbc5055281acb82b3e57f7dba2e4b04d9535058fab695660a14bf8890"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path)\n VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', $2, $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "27f70ebe788cca2e88732d8bf978883037bebca4cf75ba459858e4fb197f940b"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounce_batch",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "2a95f18e80c55a7e8178a4bd2b781d41fa47efd4da5bb9bc2d72b9aa1e33617f"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path)\n VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', 'ws2', 'f/test/flow')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "4010328a9f1611064f497726b69c08625a55a4dab25c3d9b5ece07e44d14915b"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO debounce_key (job_id, key)\n VALUES ($1, $2)\n ON CONFLICT (key)\n DO UPDATE SET\n previous_job_id = debounce_key.job_id,\n job_id = EXCLUDED.job_id, -- replace current job with new one \n debounced_times = debounce_key.debounced_times + 1 -- evaluated only if conflict,\n -- conflict means there is already existing value,\n -- which means overriding it will also imply adding new entry to v2_job_debounce_batch and thus debouncing the job\n -- so the counter should be incremented\n RETURNING\n debounced_times,\n first_started_at,\n previous_job_id AS job_id_to_debounce\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounced_times",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "first_started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "job_id_to_debounce",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "454ace9ce391725ef4f4c129cd66e4c12a5c40f512b70551958178c8b4d6c183"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "WITH ids AS (\n SELECT id as job_id FROM v2_job_debounce_batch WHERE debounce_batch = (\n SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1\n )\n ) SELECT args->>'items' FROM ids LEFT JOIN v2_job ON v2_job.id = ids.job_id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "48536968f4173715d4ef8293683c2a3eb4bd22fbe18c34890a3dc4e96e4e6133"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n SELECT unnest($1::uuid[]), 'test-workspace', now(), 'deno'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "539d661500254e2e346490710f5772cb88a1ab6bbddd97a77e06644ac0f61762"
}

View File

@@ -1,18 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "WITH job_result AS (\n SELECT result\n FROM v2_job_completed\n WHERE id = $1\n ),\n updated_queue AS (\n UPDATE v2_job_queue\n SET running = false,\n tag = COALESCE($3, tag)\n WHERE id = $2\n )\n UPDATE v2_job\n SET\n tag = COALESCE($3, tag),\n concurrent_limit = COALESCE($4, concurrent_limit),\n concurrency_time_window_s = COALESCE($5, concurrency_time_window_s),\n args = COALESCE(\n CASE\n WHEN job_result.result IS NULL THEN NULL\n WHEN jsonb_typeof(job_result.result) = 'object'\n THEN job_result.result\n WHEN jsonb_typeof(job_result.result) = 'null'\n THEN NULL\n ELSE jsonb_build_object('value', job_result.result)\n END,\n '{}'::jsonb\n ),\n preprocessed = TRUE\n FROM job_result\n WHERE v2_job.id = $2;\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Varchar",
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "5b8c1803f0ccead11517fbc8a9bdc0227dc3922217fa18f0b71ff0484d65838c"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE debounce_key SET first_started_at = now() - interval '20 seconds' WHERE key = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "66342c32f7ae0238803cb1896d9f23a74b64573f77dd32189a25b6e8369f147b"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path)\n SELECT unnest($1::uuid[]), 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace', 'f/test/flow'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "66faba2137791e0cb1353545c06f9f7c23a1559e7a761db7c2195736b8b30709"
}

View File

@@ -0,0 +1,19 @@
{
"db_name": "PostgreSQL",
"query": "WITH job_result AS (\n SELECT result\n FROM v2_job_completed\n WHERE id = $1\n ),\n updated_queue AS (\n UPDATE v2_job_queue\n SET running = false,\n tag = COALESCE($3, tag),\n scheduled_for = COALESCE($6, scheduled_for)\n WHERE id = $2\n )\n UPDATE v2_job\n SET\n tag = COALESCE($3, tag),\n concurrent_limit = COALESCE($4, concurrent_limit),\n concurrency_time_window_s = COALESCE($5, concurrency_time_window_s),\n args = COALESCE(\n CASE\n WHEN job_result.result IS NULL THEN NULL\n WHEN jsonb_typeof(job_result.result) = 'object'\n THEN job_result.result\n WHEN jsonb_typeof(job_result.result) = 'null'\n THEN NULL\n ELSE jsonb_build_object('value', job_result.result)\n END,\n '{}'::jsonb\n ),\n preprocessed = TRUE\n FROM job_result\n WHERE v2_job.id = $2;\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Varchar",
"Int4",
"Int4",
"Timestamptz"
]
},
"nullable": []
},
"hash": "79b437ad31ddab94310989b8fb6a1c130b9be1ab4b6a100fffffd687677b9c92"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT logs as \"logs!\" FROM job_logs WHERE job_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "logs!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true
]
},
"hash": "7ca599330c9913c7e66b27e2ffcfa18d53cbdd16e179749f0aea7980a901b23c"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n VALUES ($1, $2, now(), 'deno')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": []
},
"hash": "7ca7dabfe360845a5b57552b0d02267d5dbbc488bc7ab990c0bda1594bf5ef3a"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_debounce_batch (id, debounce_batch)\n -- if it the first one, nextval will be evaluated, otherwise take from the job we will debounce\n SELECT\n $2,\n COALESCE(\n (\n SELECT debounce_batch\n FROM v2_job_debounce_batch\n WHERE id = $1\n LIMIT 1\n ), -- maybe use current batch\n nextval('debounce_batch_seq')\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "8360ab72d60f07dde6ecae599e6531b5b86862029ab51fdbdd44ec16239108e2"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_settings (workspace_id) VALUES ('ws2')",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "8cf5af21cde4e4de45f995efa2a9b56ce20c26869ca78d7e17b3504b92ae85b1"
}

View File

@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT job_id, previous_job_id, debounced_times FROM debounce_key WHERE key = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "previous_job_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "debounced_times",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
false
]
},
"hash": "8f442110817244aa9533b014aa3d74a6582937dfe4759932b63b8e531984008e"
}

View File

@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH dk AS (\n INSERT INTO debounce_key (job_id, key)\n VALUES ($1, $2)\n ON CONFLICT (key)\n DO UPDATE SET\n previous_job_id = debounce_key.job_id,\n job_id = EXCLUDED.job_id,\n debounced_times = debounce_key.debounced_times + 1\n RETURNING\n debounced_times,\n first_started_at,\n previous_job_id AS job_id_to_debounce\n ), _batch AS (\n INSERT INTO v2_job_debounce_batch (id, debounce_batch)\n SELECT\n $1,\n COALESCE(\n (SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = dk.job_id_to_debounce LIMIT 1),\n nextval('debounce_batch_seq')\n )\n FROM dk\n )\n SELECT debounced_times, first_started_at, job_id_to_debounce FROM dk\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounced_times",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "first_started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "job_id_to_debounce",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "98033aae3182bde22d5b2ff08ef6e8a4f8f3a9bf04238b33e9caf46836df73d9"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id)\n VALUES ($1, 'noop', 'deno', 'test-user', 'u/test-user', 'test@windmill.dev', $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": []
},
"hash": "9f50ec7681a1fcd11cb452c7aba7e8897e49a4b6affa4e9976680336b6bd3115"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace (id, name, owner) VALUES ('ws2', 'Workspace 2', 'test-user')",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "a057ff9f5998a162ae6de05f6127b7eefc826af7bf1bb89fd758f7c03c881033"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_runtime (id) VALUES ($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "a68754521bf751450602f04dd4243199a18885e1739a5a0e7f6100eab6f3c803"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue SET runnable_settings_handle = $1 WHERE id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Uuid"
]
},
"nullable": []
},
"hash": "abb56f78aa39c6b6ae8b0ccb7b724c1f80d717e7c9d76b287da63cb5ee8e8b25"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT 1 as x FROM v2_job_completed WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "x",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "adb98040c8039e5cc27fe0579941f723b38d1784bd2f1b16d8724f1f1612dcbf"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT 1 as x FROM v2_job_queue WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "x",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "b795dc228f93c8b9bedb4a3e7467d941819a7202214e0634580bdc2ec30f0b70"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) VALUES ($1, 'ws2', now(), 'flow')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "c1a1ae759ebb84fde3e6d2727991b2a1fcc73e520fee79653bb960dc00c3e2db"
}

View File

@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH dk AS (\n INSERT INTO debounce_key (job_id, key)\n VALUES ($1, $2)\n ON CONFLICT (key)\n DO UPDATE SET\n previous_job_id = debounce_key.job_id,\n job_id = EXCLUDED.job_id,\n debounced_times = debounce_key.debounced_times + 1\n RETURNING\n debounced_times,\n first_started_at,\n previous_job_id AS job_id_to_debounce\n ), _batch AS (\n INSERT INTO v2_job_debounce_batch (id, debounce_batch)\n SELECT\n $1,\n COALESCE(\n (SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = dk.job_id_to_debounce LIMIT 1),\n nextval('debounce_batch_seq')\n )\n FROM dk\n )\n SELECT debounced_times, first_started_at, job_id_to_debounce FROM dk\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounced_times",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "first_started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "job_id_to_debounce",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "c2347460b73ae9d3167031c263032e97ebefb46be9e58bd3da9067748075311b"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as \"count!\" FROM v2_job_queue WHERE id = ANY($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": [
null
]
},
"hash": "c63a1949247f1618f6b6acee9bf6b4d3081dfed2e6ff533dbff0bdfd52687cbb"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id)\n SELECT unnest($1::uuid[]), 'noop', 'deno', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "c6d963e5cefeea728414892df9f28a89f435fc0fb7e55f243b021200f33d2151"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH _ AS (\n UPDATE debounce_key\n SET debounced_times = 0,\n first_started_at = now(),\n previous_job_id = NULL\n WHERE job_id = $1\n )\n UPDATE v2_job_debounce_batch\n SET debounce_batch = nextval('debounce_batch_seq')\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "c9530931f670eab1208c4a284a55afdc3fcbb0eb5f98fd63e2ec89442becbfaa"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as \"count!\" FROM v2_job_completed WHERE id = ANY($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": [
null
]
},
"hash": "cc309de42a3b630bb83d1b2437633ef0b98ce5e5fba1f5c1dda3ddd874ff3a39"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = ANY($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounce_batch",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": [
false
]
},
"hash": "ccfed494a8d89eb2c88d72738c341a4dd87701b0636eb9fa001cd1d9cbcd663b"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = ANY($1) ORDER BY debounce_batch",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounce_batch",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": [
false
]
},
"hash": "d9400849888dd021b0504b93004ab5e76296ed437c210942363ba06845f9f963"
}

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path, args)\n VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', $2, $3, $4)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Varchar",
"Jsonb"
]
},
"nullable": []
},
"hash": "f8cec94b94098e752f7c71cbe5e9996410a07bacebc37ed21e0bedf1a33a8fdc"
}

View File

@@ -44,11 +44,22 @@ Windmill uses a workspace-based architecture with multiple crates:
## Enterprise Features
- Enterprise files use the `*_ee.rs` suffix
- Enterprise source is in `windmill-ee-private` folder (sibling directory at `../../windmill-ee-private`), symlinked into each crate's `src/`
- Enterprise source is in `windmill-ee-private` folder (sibling directory at `../../windmill-ee-private` or `~/windmill-ee-private`), symlinked into each crate's `src/`
- The `_ee.rs` files are gitignored in the main repo — they are tracked only in the `windmill-ee-private` repo
- You can and should modify `windmill-ee-private` directly when needed (e.g., when creating new crates that need EE code, mirror the package structure there)
- Use feature flags: `#[cfg(feature = "enterprise")]`
- Isolate enterprise code in separate modules
### EE PR Workflow (MUST DO when modifying `*_ee.rs` files)
When you modify any `*_ee.rs` file and create a PR on the windmill repo, you **MUST** also:
1. **Create a matching branch** in the `windmill-ee-private` repo (use the same branch name). If using worktrees, the EE worktree is at `~/windmill-ee-private__worktrees/<branch-name>/`
2. **Commit and push** the `_ee.rs` changes in that branch
3. **Create a PR** on `windmill-ee-private` with a link to the companion windmill PR
4. **Update `ee-repo-ref.txt`**: Run `bash write_latest_ee_ref.sh` from `backend/` to write the latest EE commit hash. **Important**: the script may fall back to `~/windmill-ee-private` (main branch) instead of the worktree — verify it wrote the correct commit hash from your branch, not from main. If wrong, manually write the correct hash.
5. **Commit `ee-repo-ref.txt`** in the windmill repo so CI picks up the correct EE ref
## Code Validation (MUST DO)
After making backend changes, you MUST run `cargo check` and fix all errors and warnings before considering the work done.

141
backend/Cargo.lock generated
View File

@@ -15725,7 +15725,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15789,7 +15789,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15802,7 +15802,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"argon2",
@@ -15940,7 +15940,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15963,7 +15963,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15976,7 +15976,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16002,7 +16002,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16012,7 +16012,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16029,7 +16029,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -16052,7 +16052,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16075,7 +16075,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16091,7 +16091,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16111,7 +16111,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16131,7 +16131,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16145,7 +16145,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16171,7 +16171,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16196,10 +16196,11 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"flate2",
"reqwest 0.13.1",
"serde",
"serde_json",
"sqlx",
@@ -16212,7 +16213,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16233,7 +16234,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16253,7 +16254,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16283,7 +16284,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16310,7 +16311,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"lazy_static",
"serde",
@@ -16322,7 +16323,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16345,7 +16346,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16359,7 +16360,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16389,7 +16390,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16403,7 +16404,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16422,7 +16423,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16521,7 +16522,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16540,7 +16541,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"regex",
"serde",
@@ -16555,7 +16556,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16579,7 +16580,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"futures",
@@ -16596,7 +16597,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16612,7 +16613,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16633,7 +16634,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16664,7 +16665,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16688,7 +16689,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-stream",
@@ -16722,7 +16723,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"futures",
@@ -16740,7 +16741,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16749,7 +16750,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16761,7 +16762,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16773,7 +16774,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"gosyn",
@@ -16785,7 +16786,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16797,7 +16798,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16809,7 +16810,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -16820,7 +16821,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16831,7 +16832,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16844,7 +16845,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16868,7 +16869,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16882,7 +16883,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16899,7 +16900,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16914,7 +16915,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16933,7 +16934,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"serde",
@@ -16944,7 +16945,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16981,7 +16982,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"const_format",
@@ -17019,7 +17020,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -17029,7 +17030,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17058,7 +17059,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -17081,7 +17082,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17114,7 +17115,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17134,7 +17135,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17168,7 +17169,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17203,7 +17204,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17226,7 +17227,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17250,7 +17251,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17274,7 +17275,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17309,7 +17310,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17337,7 +17338,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17360,7 +17361,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17378,7 +17379,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.640.0"
version = "1.642.0"
dependencies = [
"anyhow",
"async-once-cell",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.640.0"
version = "1.642.0"
authors.workspace = true
edition.workspace = true
@@ -76,7 +76,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.640.0"
version = "1.642.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -1 +1 @@
5f8105b808f3f0186fdf5132d2ee602d8a14aa17
0fede4b1086bc1456be9cc55b203228c979c5c5e

View File

@@ -54,7 +54,7 @@ use windmill_common::{
HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING,
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING,
NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING,
NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING,
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
@@ -89,9 +89,9 @@ use windmill_worker::{
result_processor::handle_job_error, JobCompletedSender, JobIsolationLevel,
OtelTracingProxySettings, SameWorkerSender, BUNFIG_INSTALL_SCOPES, CARGO_REGISTRIES,
INSTANCE_PYTHON_VERSION, JAVA_HOME_DIR, JOB_DEFAULT_TIMEOUT, JOB_ISOLATION, KEEP_JOB_DIR,
MAVEN_REPOS, MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPM_CONFIG_REGISTRY, NSJAIL_AVAILABLE,
NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL,
POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL, UV_INDEX_STRATEGY,
MAVEN_REPOS, MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPMRC, NPM_CONFIG_REGISTRY,
NSJAIL_AVAILABLE, NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL,
PIP_INDEX_URL, POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL, UV_INDEX_STRATEGY,
};
#[cfg(feature = "parquet")]
@@ -330,6 +330,7 @@ pub async fn initial_load(
reload_uv_index_strategy_setting(&conn).await;
reload_npm_config_registry_setting(&conn).await;
reload_bunfig_install_scopes_setting(&conn).await;
reload_npmrc_setting(&conn).await;
reload_instance_python_version_setting(&conn).await;
reload_nuget_config_setting(&conn).await;
reload_powershell_repo_url_setting(&conn).await;
@@ -1306,6 +1307,10 @@ pub async fn reload_bunfig_install_scopes_setting(conn: &Connection) {
.await;
}
pub async fn reload_npmrc_setting(conn: &Connection) {
reload_option_setting_with_tracing(conn, NPMRC_SETTING, "NPMRC", NPMRC.clone()).await;
}
pub async fn reload_nuget_config_setting(conn: &Connection) {
reload_option_setting_with_tracing(
conn,

474
backend/test_debounce_e2e.sh Executable file
View File

@@ -0,0 +1,474 @@
#!/usr/bin/env bash
# End-to-end debounce tests against the running backend API
# Usage: BACKEND_PORT=8030 ./test_debounce_e2e.sh
set -uo pipefail
BASE="http://localhost:${BACKEND_PORT:-8030}/api"
W="admins"
EMAIL="admin@windmill.dev"
PASSWORD="changeme"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
pass=0
fail=0
log_pass() { echo -e "${GREEN}PASS${NC}: $1"; ((pass++)) || true; }
log_fail() { echo -e "${RED}FAIL${NC}: $1$2"; ((fail++)) || true; }
log_info() { echo -e "${YELLOW}INFO${NC}: $1"; }
# Unique suffix for idempotent re-runs
TS=$(date +%s)
# --- Auth ---
log_info "Logging in..."
TOKEN=$(curl -s "$BASE/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}")
if [ -z "$TOKEN" ]; then
echo "Failed to login"; exit 1
fi
AUTH="Authorization: Bearer $TOKEN"
log_info "Logged in"
# --- Helpers ---
api() {
# Usage: api METHOD path [data]
local method="$1" path="$2" data="${3:-}"
if [ -n "$data" ]; then
curl -s "$BASE/w/$W/$path" -X "$method" -H "$AUTH" -H 'Content-Type: application/json' -d "$data"
else
curl -s "$BASE/w/$W/$path" -X "$method" -H "$AUTH"
fi
}
wait_job() {
local job_id="$1" max_wait="${2:-30}"
for _ in $(seq 1 "$max_wait"); do
local r
r=$(api GET "jobs/completed/get_result_maybe/$job_id")
if echo "$r" | jq -e '.completed == true' > /dev/null 2>&1; then
echo "$r"; return 0
fi
sleep 1
done
echo '{"completed":false,"error":"timeout"}'; return 1
}
BUN_EMPTY_LOCK=$'{"dependencies": {}}\n//bun.lock\n'
create_script() {
# Usage: create_script path language content [extra_json_fields]
# Note: lock must be non-empty; empty string ("") is treated as None by the backend
# (scripts.rs:798-800), which triggers dependency resolution instead of direct deployment.
# For bun scripts, the lock must contain "//bun.lock" as a split pattern.
local path="$1" lang="$2" content="$3" extra="${4:-}"
local json
json=$(jq -n \
--arg path "$path" \
--arg lang "$lang" \
--arg content "$content" \
--arg summary "test" \
--arg desc "test" \
--arg lock "$BUN_EMPTY_LOCK" \
'{path: $path, language: $lang, content: $content, summary: $summary, description: $desc, lock: $lock}')
if [ -n "$extra" ]; then
json=$(echo "$json" | jq ". + $extra")
fi
local hash
hash=$(api POST "scripts/create" "$json")
# Small delay for DB visibility after tx commit
sleep 0.2
echo "$hash"
}
run_script() {
# Usage: run_script path args_json
api POST "jobs/run/p/$1" "$2"
}
###############################################################################
# TEST 1: Deploy a script and run it 5 times in close succession
###############################################################################
echo ""
log_info "=== TEST 1: Deploy & run script 5 times rapidly ==="
P1="u/admin/e2e_simple_$TS"
H1=$(create_script "$P1" "bun" 'export function main(x: number = 0) { return { result: x * 2 }; }')
if echo "$H1" | grep -qE '^[0-9a-f]{16}$'; then
log_pass "Script created: $H1"
else
log_fail "Script creation" "$H1"
fi
log_info "Running 5 times rapidly..."
JOB_IDS=()
for i in $(seq 1 5); do
JID=$(run_script "$P1" "{\"x\": $i}")
JOB_IDS+=("$JID")
done
log_info "Jobs: ${JOB_IDS[*]}"
log_info "Waiting for completion..."
all_ok=true
for i in "${!JOB_IDS[@]}"; do
JID="${JOB_IDS[$i]}"
R=$(wait_job "$JID" 30)
success=$(echo "$R" | jq -r '.success // false')
value=$(echo "$R" | jq -r '.result.result // "null"')
expected=$(( (i + 1) * 2 ))
if [ "$success" = "true" ] && [ "$value" = "$expected" ]; then
log_pass "Job $((i+1)): x=$((i+1))$value (correct)"
else
log_fail "Job $((i+1))" "success=$success value=$value expected=$expected"
all_ok=false
fi
done
if [ "$all_ok" = "true" ]; then
log_pass "All 5 runs completed correctly (no debounce — different args)"
fi
###############################################################################
# TEST 2: Redeploy script WITHOUT lock in close succession
###############################################################################
echo ""
log_info "=== TEST 2: Redeploy without lock in rapid succession ==="
P2="u/admin/e2e_nolock_$TS"
# Deploy 5 versions of the same script without lock → triggers dependency jobs
DEPLOY_HASHES=()
for i in $(seq 1 5); do
content="export function main(x: number = 0) { return { result: x * $i, version: $i }; }"
parent_extra=""
if [ "${#DEPLOY_HASHES[@]}" -gt 0 ]; then
last_hash="${DEPLOY_HASHES[-1]}"
parent_extra="{\"parent_hash\": \"$last_hash\"}"
fi
# Deploy without lock (omit lock field entirely)
json=$(jq -n \
--arg path "$P2" \
--arg content "$content" \
--arg summary "v$i" \
--arg desc "test" \
'{path: $path, language: "bun", content: $content, summary: $summary, description: $desc}')
if [ -n "$parent_extra" ]; then
json=$(echo "$json" | jq ". + $parent_extra")
fi
hash=$(api POST "scripts/create" "$json")
if echo "$hash" | grep -qE '^[0-9a-f]{16}$'; then
DEPLOY_HASHES+=("$hash")
log_info "Deploy $i: $hash"
else
log_fail "Deploy $i" "$hash"
# If path conflict, the script already exists from a previous version
break
fi
sleep 0.1
done
# Wait for dependency resolution
log_info "Waiting 15s for dependency jobs..."
sleep 15
# Check the latest script — should have lock resolved
SCRIPT_INFO=$(api GET "scripts/get/p/$P2")
LOCK=$(echo "$SCRIPT_INFO" | jq -r '.lock // "null"')
if [ "$LOCK" != "null" ] && [ -n "$LOCK" ]; then
log_pass "Latest version has lock resolved"
else
log_info "Lock not yet resolved: $LOCK"
fi
# Run the latest version to verify it works
sleep 0.5
JID2=$(run_script "$P2" '{"x": 10}')
if echo "$JID2" | grep -qE '^[0-9a-f-]{36}$'; then
R2=$(wait_job "$JID2" 30)
success=$(echo "$R2" | jq -r '.success // false')
if [ "$success" = "true" ]; then
version=$(echo "$R2" | jq -r '.result.version // "?"')
log_pass "Latest version runs: version=$version"
else
err=$(echo "$R2" | jq -r '.result.error.message // "unknown"' 2>/dev/null)
log_fail "Run latest version" "success=false err=$err"
fi
else
log_fail "Run latest version" "bad job id: $JID2"
fi
###############################################################################
# TEST 3: Script with debounce_delay_s — rapid runs with SAME args
###############################################################################
echo ""
log_info "=== TEST 3: Debounce with same args (should debounce) ==="
P3="u/admin/e2e_debounce_$TS"
H3=$(create_script "$P3" "bun" \
'export function main(x: number = 0) { return { result: x }; }' \
'{"debounce_delay_s": 3}')
if echo "$H3" | grep -qE '^[0-9a-f]{16}$'; then
log_pass "Debounce script created: $H3"
else
log_fail "Debounce script creation" "$H3"
fi
log_info "Running 5 times with same args {x: 42}..."
DEB_IDS=()
for i in $(seq 1 5); do
JID=$(run_script "$P3" '{"x": 42}')
DEB_IDS+=("$JID")
log_info " Run $i: $JID"
done
log_info "Waiting 10s for debounce delay (3s) + execution..."
sleep 10
executed=0
skipped=0
for JID in "${DEB_IDS[@]}"; do
if ! echo "$JID" | grep -qE '^[0-9a-f-]{36}$'; then
log_info " Invalid job id: $JID"
continue
fi
R=$(wait_job "$JID" 5 2>/dev/null || echo '{"completed":false}')
completed=$(echo "$R" | jq -r '.completed // false')
success=$(echo "$R" | jq -r '.success // false')
if [ "$completed" = "true" ] && [ "$success" = "true" ]; then
((executed++)) || true
elif [ "$completed" = "true" ]; then
((skipped++)) || true
fi
done
log_info "Results: $executed executed, $skipped skipped out of ${#DEB_IDS[@]}"
if [ "$executed" -eq 1 ] && [ "$skipped" -ge 3 ]; then
log_pass "Debouncing perfect: 1 executed, $skipped skipped"
elif [ "$executed" -le 2 ] && [ "$skipped" -ge 2 ]; then
log_pass "Debouncing working: $executed executed, $skipped skipped"
else
log_fail "Debounce same args" "executed=$executed skipped=$skipped (want ~1 exec, ~4 skip)"
fi
###############################################################################
# TEST 3b: Different args should NOT debounce against each other
###############################################################################
echo ""
log_info "=== TEST 3b: Debounce with different args (should NOT debounce) ==="
DIFF_IDS=()
for i in $(seq 1 3); do
JID=$(run_script "$P3" "{\"x\": $((i * 100))}")
DIFF_IDS+=("$JID")
done
log_info "Waiting 8s..."
sleep 8
diff_exec=0
for JID in "${DIFF_IDS[@]}"; do
if ! echo "$JID" | grep -qE '^[0-9a-f-]{36}$'; then continue; fi
R=$(wait_job "$JID" 5 2>/dev/null || echo '{"completed":false}')
success=$(echo "$R" | jq -r '.success // false')
if [ "$success" = "true" ]; then ((diff_exec++)) || true; fi
done
if [ "$diff_exec" -eq 3 ]; then
log_pass "Different args: all 3 executed independently"
else
log_fail "Different args" "only $diff_exec/3 executed"
fi
###############################################################################
# TEST 4: Custom debounce_key with $args interpolation
###############################################################################
echo ""
log_info "=== TEST 4: Custom debounce key ==="
P4="u/admin/e2e_custom_key_$TS"
H4=$(create_script "$P4" "bun" \
'export function main(event_id: string = "", data: string = "") { return { event_id, data }; }' \
'{"debounce_delay_s": 3, "debounce_key": "event#$args.event_id"}')
if echo "$H4" | grep -qE '^[0-9a-f]{16}$'; then
log_pass "Custom key script created: $H4"
else
log_fail "Custom key script creation" "$H4"
fi
# Same event_id → should debounce
log_info "3 runs with same event_id..."
SAME_IDS=()
for i in $(seq 1 3); do
JID=$(run_script "$P4" "{\"event_id\": \"evt_001\", \"data\": \"payload_$i\"}")
SAME_IDS+=("$JID")
done
# Different event_id → should NOT debounce
JID_DIFF=$(run_script "$P4" '{"event_id": "evt_002", "data": "different"}')
log_info "Waiting 8s..."
sleep 8
same_exec=0
same_skip=0
for JID in "${SAME_IDS[@]}"; do
if ! echo "$JID" | grep -qE '^[0-9a-f-]{36}$'; then continue; fi
R=$(wait_job "$JID" 5 2>/dev/null || echo '{"completed":false}')
completed=$(echo "$R" | jq -r '.completed // false')
success=$(echo "$R" | jq -r '.success // false')
if [ "$completed" = "true" ] && [ "$success" = "true" ]; then
data=$(echo "$R" | jq -r '.result.data // "?"')
((same_exec++)) || true
log_info " Executed: data=$data"
elif [ "$completed" = "true" ]; then
((same_skip++)) || true
fi
done
log_info "Same event_id: $same_exec executed, $same_skip skipped"
if [ "$same_exec" -eq 1 ] && [ "$same_skip" -ge 1 ]; then
log_pass "Custom key debounce: same event_id debounced correctly"
elif [ "$same_exec" -le 2 ]; then
log_pass "Custom key debounce working: $same_exec executed, $same_skip skipped"
else
log_fail "Custom key debounce" "exec=$same_exec skip=$same_skip"
fi
# Check different event_id ran independently
if echo "$JID_DIFF" | grep -qE '^[0-9a-f-]{36}$'; then
R_DIFF=$(wait_job "$JID_DIFF" 10 2>/dev/null || echo '{"completed":false}')
diff_success=$(echo "$R_DIFF" | jq -r '.success // false')
if [ "$diff_success" = "true" ]; then
log_pass "Different event_id: executed independently"
else
log_info "Different event_id: success=$diff_success"
fi
fi
###############################################################################
# TEST 5: Git sync with bad target — debounced deployment callbacks
###############################################################################
echo ""
log_info "=== TEST 5: Git sync debounce + aggregation ==="
# Create git repo resource
api POST "resources/create?update_if_exists=true" '{
"path": "u/admin/e2e_bad_git_repo",
"description": "Bad git repo for testing",
"resource_type": "git_repository",
"value": {"url": "https://github.com/nonexistent/nope.git", "branch": "main", "token": "bad"}
}' > /dev/null 2>&1
log_info "Created git repo resource"
# Create a sync script at a folder path where the 2nd segment is a number >= 28103.
# is_script_meets_min_version parses split("/").skip(1).next() as the version number.
# This enables debounce_delay_s=5 and debounce_args_to_accumulate=["items"].
api POST "folders/create" '{"name": "28103"}' > /dev/null 2>&1
P5="f/28103/e2e_sync_$TS"
H5=$(create_script "$P5" "bun" \
'export function main(repo_url_resource_path: string = "", workspace_id: string = "", items: any[] = [], use_individual_branch: boolean = false, group_by_folder: boolean = false, parent_workspace_id: string = "") { return { synced: items.length, items }; }')
if echo "$H5" | grep -qE '^[0-9a-f]{16}$'; then
log_pass "Sync script created: $H5"
else
log_fail "Sync script creation" "$H5"
fi
# Configure git sync with include_path to match deployed scripts.
# Without include_path, path_matches_filters returns false and no DeploymentCallback is created.
api POST "workspaces/edit_git_sync_config" "{
\"git_sync_settings\": {
\"include_type\": [\"script\"],
\"include_path\": [\"**\"],
\"repositories\": [{
\"script_path\": \"$P5\",
\"git_repo_resource_path\": \"\$res:u/admin/e2e_bad_git_repo\",
\"use_individual_branch\": false,
\"group_by_folder\": false
}]
}
}" > /dev/null 2>&1
log_pass "Git sync configured with include_path and versioned folder script path"
# Deploy 5 scripts rapidly to trigger git sync.
# Scripts are created with lock="" (via create_script), so handle_deployment_metadata
# fires immediately after tx commit (not after dependency resolution).
log_info "Deploying 5 scripts to trigger git sync..."
for i in $(seq 1 5); do
dp="u/admin/e2e_gitsync_${TS}_$i"
create_script "$dp" "bun" "export function main() { return { v: $i }; }" > /dev/null
log_info " Deployed $dp"
done
# Wait for debounce delay (5s) + execution
log_info "Waiting 15s for debounce (5s) + execution..."
sleep 15
# Check deployment callback jobs for our sync script.
# Debounced jobs have is_skipped=true (but success=true), so we use is_skipped to distinguish.
SYNC_JOBS=$(api GET "jobs/completed/list?script_path_exact=$P5&job_kinds=deploymentcallback")
SYNC_TOTAL=$(echo "$SYNC_JOBS" | jq 'length')
SYNC_EXECUTED=$(echo "$SYNC_JOBS" | jq '[.[] | select(.is_skipped != true)] | length')
SYNC_SKIPPED=$(echo "$SYNC_JOBS" | jq '[.[] | select(.is_skipped == true)] | length')
log_info "Sync jobs: total=$SYNC_TOTAL executed=$SYNC_EXECUTED skipped=$SYNC_SKIPPED"
if [ "$SYNC_TOTAL" -gt 0 ]; then
# With debouncing (5s delay), rapid deploys should be consolidated.
# All 5 jobs are created but most should be skipped (debounced).
if [ "$SYNC_SKIPPED" -gt 0 ]; then
log_pass "Git sync debouncing: $SYNC_EXECUTED executed, $SYNC_SKIPPED debounced out of $SYNC_TOTAL"
else
log_fail "Git sync debouncing" "No jobs were debounced ($SYNC_TOTAL all executed independently)"
fi
# Check if items were aggregated in the executed (non-skipped) job(s)
for idx in $(seq 0 $((SYNC_TOTAL - 1))); do
is_skipped=$(echo "$SYNC_JOBS" | jq -r ".[$idx].is_skipped")
[ "$is_skipped" = "true" ] && continue
jid=$(echo "$SYNC_JOBS" | jq -r ".[$idx].id")
r=$(api GET "jobs/completed/get_result/$jid")
items_count=$(echo "$r" | jq '.items | length // 0')
log_info " Executed sync job $jid: items=$items_count"
if [ "$items_count" -gt 1 ]; then
log_pass "Items aggregated: $items_count items in single sync job"
fi
done
else
# Check queued — jobs may still be pending debounce delay
Q=$(api GET "jobs/queue/list?script_path_exact=$P5&job_kinds=deploymentcallback")
QC=$(echo "$Q" | jq 'length')
log_info "No completed sync jobs. $QC queued."
if [ "$QC" -gt 0 ] && [ "$QC" -lt 5 ]; then
log_pass "Git sync debouncing (queued): $QC jobs for 5 deploys"
elif [ "$QC" -eq 0 ]; then
log_fail "Git sync" "No deployment callback jobs found (completed or queued)"
fi
fi
# Cleanup git sync
api POST "workspaces/edit_git_sync_config" '{"git_sync_settings": null}' > /dev/null 2>&1
log_info "Git sync config cleared"
###############################################################################
# Summary
###############################################################################
echo ""
echo "========================================="
echo -e "Results: ${GREEN}$pass passed${NC}, ${RED}$fail failed${NC}"
echo "========================================="
if [ "$fail" -gt 0 ]; then
exit 1
fi

View File

@@ -1,8 +1,8 @@
use windmill_test_utils::*;
use sqlx::postgres::Postgres;
use sqlx::Pool;
use windmill_common::jobs::{JobPayload, RawCode};
use windmill_common::scripts::ScriptLang;
use windmill_test_utils::*;
// ============================================================================
// Basic Execution Tests
@@ -27,8 +27,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -63,8 +63,8 @@ export function main(name: string, count: number) {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -104,8 +104,9 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -135,8 +136,9 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -167,8 +169,9 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -207,8 +210,8 @@ export async function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -245,8 +248,9 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -276,8 +280,9 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -318,8 +323,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -358,8 +363,8 @@ export function notMain() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -398,8 +403,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -437,8 +442,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -474,8 +479,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -516,8 +521,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -613,8 +618,9 @@ export function main() {
path: Some("f/nested/test_deep".to_string()),
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -647,8 +653,9 @@ export function main() {
path: Some("f/nested/test_deep_relative".to_string()),
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -693,8 +700,8 @@ export function main() {
path: Some("f/circular/test_both".to_string()),
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -741,8 +748,8 @@ export function main(x: number) {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -791,8 +798,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -836,8 +843,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -859,11 +866,11 @@ export function main() {
// ============================================================================
mod dedicated_worker_protocol {
use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult};
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult};
use windmill_worker::{
build_loader, generate_dedicated_worker_wrapper, BUN_DEDICATED_WORKER_ARGS, LoaderMode,
build_loader, generate_dedicated_worker_wrapper, LoaderMode, BUN_DEDICATED_WORKER_ARGS,
BUN_PATH, NODE_BIN_PATH,
};
@@ -934,12 +941,8 @@ mod dedicated_worker_protocol {
let temp_dir = tempfile::tempdir().unwrap();
// Create files and get the wrapper path (bundled for node, raw for bun)
let wrapper_path = create_test_worker_files(
temp_dir.path(),
script,
arg_names,
runtime == "node",
);
let wrapper_path =
create_test_worker_files(temp_dir.path(), script, arg_names, runtime == "node");
let wrapper_str = wrapper_path.to_str().unwrap();
// Build args matching production behavior
@@ -992,7 +995,10 @@ mod dedicated_worker_protocol {
match parse_dedicated_worker_line(response.trim()) {
DedicatedWorkerResult::Success(value) => results.push(Ok(value)),
DedicatedWorkerResult::Error(err) => {
let msg = err["message"].as_str().unwrap_or("Unknown error").to_string();
let msg = err["message"]
.as_str()
.unwrap_or("Unknown error")
.to_string();
results.push(Err(msg));
}
other => panic!("Unexpected response: {:?}", other),
@@ -1162,8 +1168,8 @@ export function main(name: string) {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -1190,6 +1196,68 @@ export function main(name: string) {
Ok(())
}
/// Test that full .npmrc content works for bun jobs with private registries.
/// Requires:
/// - `TEST_NPMRC` environment variable set to the full .npmrc content
#[cfg(feature = "private_registry_test")]
#[sqlx::test(fixtures("base"))]
async fn test_bun_job_private_npmrc(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_worker::NPMRC;
let npmrc_content = std::env::var("TEST_NPMRC")
.expect("TEST_NPMRC must be set when running private_registry_test");
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
{
let mut npmrc = NPMRC.write().await;
*npmrc = Some(npmrc_content.clone());
}
let content = r#"
import { greet } from "@windmill-test/private-pkg";
export function main(name: string) {
return greet(name);
}
"#
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
});
let result = RunJob::from(job)
.arg("name", serde_json::json!("World"))
.run_until_complete(&db, false, port)
.await
.json_result()
.unwrap();
{
let mut npmrc = NPMRC.write().await;
*npmrc = None;
}
assert_eq!(
result,
serde_json::json!("Hello from private package, World!")
);
Ok(())
}
/// Tests for RELATIVE_BUN_BUILDER (loader_builder.bun.js)
/// These tests verify Bun's behavior for import scanning and package.json generation.
/// Purpose: Catch regressions when upgrading Bun versions.
@@ -1241,8 +1309,8 @@ mod bun_builder_tests {
}
// Read generated package.json
let package_json = std::fs::read_to_string(dir.join("package.json"))
.expect("package.json not generated");
let package_json =
std::fs::read_to_string(dir.join("package.json")).expect("package.json not generated");
serde_json::from_str(&package_json).expect("Invalid JSON in package.json")
}
@@ -1257,7 +1325,10 @@ export function main() { return lodash; }
let pkg = run_builder(main_ts);
let deps = pkg["dependencies"].as_object().unwrap();
assert!(deps.contains_key("lodash"), "lodash should be in dependencies");
assert!(
deps.contains_key("lodash"),
"lodash should be in dependencies"
);
assert_eq!(deps["lodash"], "latest");
}
@@ -1271,7 +1342,10 @@ export function main() { return _; }
let pkg = run_builder(main_ts);
let deps = pkg["dependencies"].as_object().unwrap();
assert!(deps.contains_key("lodash"), "lodash should be in dependencies");
assert!(
deps.contains_key("lodash"),
"lodash should be in dependencies"
);
assert_eq!(deps["lodash"], "4.17.21");
}
@@ -1304,9 +1378,18 @@ export function main() { return { lodash, axios, dayjs }; }
let pkg = run_builder(main_ts);
let deps = pkg["dependencies"].as_object().unwrap();
assert!(deps.contains_key("lodash"), "lodash should be in dependencies");
assert!(deps.contains_key("axios"), "axios should be in dependencies");
assert!(deps.contains_key("dayjs"), "dayjs should be in dependencies");
assert!(
deps.contains_key("lodash"),
"lodash should be in dependencies"
);
assert!(
deps.contains_key("axios"),
"axios should be in dependencies"
);
assert!(
deps.contains_key("dayjs"),
"dayjs should be in dependencies"
);
assert_eq!(deps.len(), 3, "Should have exactly 3 dependencies");
}
@@ -1330,8 +1413,15 @@ export function main() { return { fs, path, lodash }; }
!deps.contains_key("path"),
"path (builtin) should NOT be in dependencies"
);
assert!(deps.contains_key("lodash"), "lodash should be in dependencies");
assert_eq!(deps.len(), 1, "Should have exactly 1 dependency (lodash only)");
assert!(
deps.contains_key("lodash"),
"lodash should be in dependencies"
);
assert_eq!(
deps.len(),
1,
"Should have exactly 1 dependency (lodash only)"
);
}
/// Test: semver.order() resolves version conflicts (picks lowest version)
@@ -1347,7 +1437,10 @@ export function main() { return { a, b }; }
let pkg = run_builder(main_ts);
let deps = pkg["dependencies"].as_object().unwrap();
assert!(deps.contains_key("lodash"), "lodash should be in dependencies");
assert!(
deps.contains_key("lodash"),
"lodash should be in dependencies"
);
// The builder sorts by semver and picks the first (lowest) version
assert_eq!(
deps["lodash"], "4.17.10",

View File

@@ -0,0 +1,10 @@
-- Fixture for testing wmill CLI variable/resource get from bash scripts
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES ('test-workspace', 'u/test-user/test_var', 'hello from variable', false, 'A test variable', '{"u/test-user": true}');
INSERT INTO resource_type (workspace_id, name, schema, description, created_by)
VALUES ('test-workspace', 'test_object', '{}', 'Test object type', 'test-user');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/test_res', '{"host": "localhost", "port": 5432}', 'A test resource', 'test_object', '{"u/test-user": true}', 'test-user');

View File

@@ -993,6 +993,80 @@ echo "hello $msg"
Ok(())
}
#[sqlx::test(fixtures("base", "wmill_cli_test"))]
async fn test_bash_wmill_variable_get(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// The bash script uses wmill CLI to get the variable value.
// The worker sets WM_TOKEN, WM_WORKSPACE, and BASE_INTERNAL_URL as env vars,
// and the CLI auto-configures from them when no workspace is explicitly set.
// We point WMILL_CONFIG_DIR to a clean temp dir so no local active workspace interferes.
let content = r#"
export WMILL_CONFIG_DIR=$(mktemp -d)
result=$(wmill variable get "u/test-user/test_var" --json | jq -r .value)
echo "$result"
"#
.to_owned();
let job = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
lock: None,
language: ScriptLang::Bash,
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
}))
.run_until_complete(&db, false, port)
.await;
assert_eq!(job.json_result(), Some(json!("hello from variable")));
Ok(())
}
#[sqlx::test(fixtures("base", "wmill_cli_test"))]
async fn test_bash_wmill_resource_get(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// The bash script uses wmill CLI to get the resource value.
// We point WMILL_CONFIG_DIR to a clean temp dir so no local active workspace interferes.
let content = r#"
export WMILL_CONFIG_DIR=$(mktemp -d)
result=$(wmill resource get "u/test-user/test_res" --json | jq -c .value)
echo "$result"
"#
.to_owned();
let job = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
lock: None,
language: ScriptLang::Bash,
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
}))
.run_until_complete(&db, false, port)
.await;
// Bash echo outputs are returned as strings, so the JSON is a string value
assert_eq!(
job.json_result(),
Some(json!("{\"host\":\"localhost\",\"port\":5432}"))
);
Ok(())
}
#[cfg(feature = "nu")]
#[sqlx::test(fixtures("base"))]
async fn test_nu_job(db: Pool<Postgres>) -> anyhow::Result<()> {
@@ -1592,6 +1666,66 @@ export async function main(a: Date) {
Ok(())
}
/// Test that full .npmrc content works for deno jobs with private registries.
/// Requires:
/// - `TEST_NPMRC` environment variable set to the full .npmrc content
#[cfg(feature = "private_registry_test")]
#[sqlx::test(fixtures("base"))]
async fn test_deno_job_private_npmrc(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_worker::NPMRC;
let npmrc_content = std::env::var("TEST_NPMRC")
.expect("TEST_NPMRC must be set when running private_registry_test");
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
{
let mut npmrc = NPMRC.write().await;
*npmrc = Some(npmrc_content.clone());
}
let content = r#"
import { greet } from "npm:@windmill-test/private-pkg";
export function main(name: string) {
return greet(name);
}
"#
.to_owned();
let result = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Deno,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
}))
.arg("name", json!("World"))
.run_until_complete(&db, false, port)
.await
.json_result()
.unwrap();
{
let mut npmrc = NPMRC.write().await;
*npmrc = None;
}
assert_eq!(
result,
serde_json::json!("Hello from private package, World!")
);
Ok(())
}
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base"))]
async fn test_python_job_datetime_and_bytes(db: Pool<Postgres>) -> anyhow::Result<()> {

View File

@@ -6,7 +6,6 @@
* LICENSE-AGPL for a copy of the license.
*/
use windmill_api_auth::ApiAuthed;
use axum::{
extract::{Path, Query},
routing::{get, post},
@@ -20,6 +19,7 @@ use std::{
fmt::{Display, Formatter},
vec,
};
use windmill_api_auth::ApiAuthed;
use windmill_common::{
db::UserDB,
error::JsonResult,
@@ -352,11 +352,12 @@ async fn update_input(
) -> JsonResult<String> {
let mut tx = user_db.begin(&authed).await?;
sqlx::query("UPDATE input SET name = $1, is_public = $2 WHERE id = $3 and workspace_id = $4")
sqlx::query("UPDATE input SET name = $1, is_public = $2 WHERE id = $3 and workspace_id = $4 AND created_by = $5")
.bind(&input.name)
.bind(&input.is_public)
.bind(&input.id)
.bind(&w_id)
.bind(&authed.username)
.execute(&mut *tx)
.await?;
@@ -372,9 +373,10 @@ async fn delete_input(
) -> JsonResult<String> {
let mut tx = user_db.begin(&authed).await?;
sqlx::query("DELETE FROM input WHERE id = $1 and workspace_id = $2")
sqlx::query("DELETE FROM input WHERE id = $1 and workspace_id = $2 AND created_by = $3")
.bind(&i_id)
.bind(&w_id)
.bind(&authed.username)
.execute(&mut *tx)
.await?;

View File

@@ -13,6 +13,7 @@ windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
axum.workspace = true
flate2.workspace = true
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true

View File

@@ -14,8 +14,10 @@ use std::collections::HashMap;
use tower_http::cors::{Any, CorsLayer};
use windmill_common::{
error::{Error, JsonResult, Result},
global_settings::{load_value_from_global_settings, NPM_CONFIG_REGISTRY_SETTING},
utils::StripPath,
global_settings::{
load_value_from_global_settings, NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING,
},
utils::{parse_npmrc_registry, StripPath},
};
use windmill_api_auth::ApiAuthed;
@@ -129,6 +131,14 @@ pub fn workspaced_service() -> Router {
)
}
fn build_registry_request(url: &str, auth_token: &Option<String>) -> reqwest::RequestBuilder {
let mut req = HTTP_CLIENT.get(url);
if let Some(token) = auth_token {
req = req.bearer_auth(token);
}
req
}
/// Get package metadata (versions and tags) from the private registry
async fn get_package_metadata(
_authed: ApiAuthed,
@@ -136,21 +146,14 @@ async fn get_package_metadata(
Extension(db): Extension<sqlx::Pool<sqlx::Postgres>>,
) -> JsonResult<PackageVersions> {
let package = parse_package_name(package_path.to_path());
let npm_registry = get_npm_registry(&db).await?;
if npm_registry.is_none() {
return Err(Error::BadRequest(
"No private npm registry configured".to_string(),
));
}
let registry_url = npm_registry.unwrap();
let (registry_url, auth_token) = get_npm_registry(&db)
.await?
.ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?;
let package_url = format_registry_url(&registry_url, &package, None, None);
tracing::info!("Fetching package metadata from: {}", package_url);
let response = HTTP_CLIENT
.get(&package_url)
let response = build_registry_request(&package_url, &auth_token)
.send()
.await
.map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?;
@@ -167,7 +170,6 @@ async fn get_package_metadata(
.await
.map_err(|e| Error::InternalErr(format!("Failed to parse package metadata: {}", e)))?;
// Extract versions and dist-tags from the package metadata
let mut versions = Vec::new();
let mut tags = HashMap::new();
@@ -194,22 +196,15 @@ async fn resolve_package_version(
Extension(db): Extension<sqlx::Pool<sqlx::Postgres>>,
) -> JsonResult<PackageVersion> {
let package = parse_package_name(package_path.to_path());
let npm_registry = get_npm_registry(&db).await?;
if npm_registry.is_none() {
return Err(Error::BadRequest(
"No private npm registry configured".to_string(),
));
}
let registry_url = npm_registry.unwrap();
let (registry_url, auth_token) = get_npm_registry(&db)
.await?
.ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?;
let reference = query.tag.unwrap_or_else(|| "latest".to_string());
let package_url = format_registry_url(&registry_url, &package, None, None);
tracing::info!("Resolving package version from: {}", package_url);
let response = HTTP_CLIENT
.get(&package_url)
let response = build_registry_request(&package_url, &auth_token)
.send()
.await
.map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?;
@@ -256,21 +251,14 @@ async fn get_package_filetree(
Extension(db): Extension<sqlx::Pool<sqlx::Postgres>>,
) -> JsonResult<PackageFiletree> {
let (package, version) = parse_package_and_version(package_version_path.to_path())?;
let npm_registry = get_npm_registry(&db).await?;
if npm_registry.is_none() {
return Err(Error::BadRequest(
"No private npm registry configured".to_string(),
));
}
let registry_url = npm_registry.unwrap();
let (registry_url, auth_token) = get_npm_registry(&db)
.await?
.ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?;
let package_url = format_registry_url(&registry_url, &package, None, None);
tracing::info!("Fetching package filetree from: {}", package_url);
let response = HTTP_CLIENT
.get(&package_url)
let response = build_registry_request(&package_url, &auth_token)
.send()
.await
.map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?;
@@ -287,7 +275,6 @@ async fn get_package_filetree(
.await
.map_err(|e| Error::InternalErr(format!("Failed to parse package metadata: {}", e)))?;
// Get the tarball URL for this version
let tarball_url = package_json
.get("versions")
.and_then(|v| v.get(&version))
@@ -296,9 +283,7 @@ async fn get_package_filetree(
.and_then(|t| t.as_str())
.ok_or_else(|| Error::NotFound(format!("Tarball not found for {}@{}", package, version)))?;
// Download and extract tarball to get file list
let tarball_response = HTTP_CLIENT
.get(tarball_url)
let tarball_response = build_registry_request(tarball_url, &auth_token)
.send()
.await
.map_err(|e| Error::InternalErr(format!("Failed to download tarball: {}", e)))?;
@@ -337,21 +322,14 @@ async fn get_package_file(
Extension(db): Extension<sqlx::Pool<sqlx::Postgres>>,
) -> Result<String> {
let (package, version, filepath) = parse_package_version_and_file(full_path.to_path())?;
let npm_registry = get_npm_registry(&db).await?;
if npm_registry.is_none() {
return Err(Error::BadRequest(
"No private npm registry configured".to_string(),
));
}
let registry_url = npm_registry.unwrap();
let (registry_url, auth_token) = get_npm_registry(&db)
.await?
.ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?;
let package_url = format_registry_url(&registry_url, &package, None, None);
tracing::info!("Fetching package file from: {}", package_url);
let response = HTTP_CLIENT
.get(&package_url)
let response = build_registry_request(&package_url, &auth_token)
.send()
.await
.map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?;
@@ -368,7 +346,6 @@ async fn get_package_file(
.await
.map_err(|e| Error::InternalErr(format!("Failed to parse package metadata: {}", e)))?;
// Get the tarball URL for this version
let tarball_url = package_json
.get("versions")
.and_then(|v| v.get(&version))
@@ -377,9 +354,7 @@ async fn get_package_file(
.and_then(|t| t.as_str())
.ok_or_else(|| Error::NotFound(format!("Tarball not found for {}@{}", package, version)))?;
// Download tarball
let tarball_response = HTTP_CLIENT
.get(tarball_url)
let tarball_response = build_registry_request(tarball_url, &auth_token)
.send()
.await
.map_err(|e| Error::InternalErr(format!("Failed to download tarball: {}", e)))?;
@@ -402,13 +377,38 @@ async fn get_package_file(
Ok(file_content)
}
/// Get the npm registry URL from global settings
async fn get_npm_registry(db: &sqlx::Pool<sqlx::Postgres>) -> Result<Option<String>> {
/// Get the npm registry URL and optional auth token from global settings.
/// Checks the `npmrc` setting first, then falls back to `npm_config_registry`.
async fn get_npm_registry(
db: &sqlx::Pool<sqlx::Postgres>,
) -> Result<Option<(String, Option<String>)>> {
let npmrc = load_value_from_global_settings(db, NPMRC_SETTING)
.await?
.and_then(|v| v.as_str().map(|s| s.to_string()));
if let Some(ref npmrc_content) = npmrc {
if let Some(parsed) = parse_npmrc_registry(npmrc_content) {
return Ok(Some(parsed));
}
}
let registry = load_value_from_global_settings(db, NPM_CONFIG_REGISTRY_SETTING)
.await?
.and_then(|v| v.as_str().map(|s| s.to_string()));
Ok(registry)
if let Some(ref s) = registry {
let (url, token) = if s.contains(":_authToken=") {
let parts: Vec<&str> = s.split(":_authToken=").collect();
let url = parts[0].to_string();
let token = parts.get(1).map(|t| t.to_string());
(url, token)
} else {
(s.clone(), None)
};
return Ok(Some((url, token)));
}
Ok(None)
}
/// Format a registry URL for a package

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.640.0
version: 1.642.0
title: Windmill API
contact:
@@ -19671,6 +19671,8 @@ components:
type: boolean
lock:
type: string
flow_path:
type: string
required:
- args

View File

@@ -91,8 +91,8 @@ impl RawWebhookArgs {
get_random_file_name, get_workspace_s3_resource, upload_file_internal,
};
use futures::TryStreamExt;
use windmill_object_store::object_store_reexports::{Attribute, Attributes};
use windmill_object_store::build_object_store_client;
use windmill_object_store::object_store_reexports::{Attribute, Attributes};
let (_, s3_resource) = get_workspace_s3_resource(authed, db, None, w_id, None).await?;
@@ -106,20 +106,24 @@ impl RawWebhookArgs {
Error::BadRequest(format!("Error reading multipart field: {}", e.body_text()))
})? {
if let Some(name) = field.name().map(|x| x.to_string()) {
if let Some(content_type) = field.content_type() {
if field.file_name().is_some() {
let content_type = field
.content_type()
.unwrap_or("application/octet-stream")
.to_string();
let ext = field
.file_name()
.map(|x| x.split('.').last())
.flatten()
.and_then(|x| x.split('.').last())
.map(|x| x.to_string());
let filename = field.file_name().map(|x| x.to_string());
let file_key = get_random_file_name(ext);
let options = Attributes::from_iter(vec![
(Attribute::ContentType, content_type.to_string()),
(Attribute::ContentType, content_type),
(
Attribute::ContentDisposition,
if let Some(filename) = field.file_name() {
if let Some(filename) = filename {
format!("inline; filename=\"{}\"", filename)
} else {
"inline".to_string()

View File

@@ -42,8 +42,6 @@ use windmill_common::runnable_settings::{
};
#[cfg(feature = "inline_preview")]
use windmill_common::runtime_assets::{register_runtime_asset, InsertRuntimeAssetParams};
use windmill_types::s3::BundleFormat;
use windmill_object_store::upload_artifact_to_store;
use windmill_common::scripts::ScriptRunnableSettingsInline;
use windmill_common::triggers::TriggerMetadata;
use windmill_common::utils::{RunnableKind, WarnAfterExt};
@@ -54,8 +52,10 @@ use windmill_common::workspace_dependencies::{
use windmill_common::DYNAMIC_INPUT_CACHE;
#[cfg(all(feature = "enterprise", feature = "smtp"))]
use windmill_common::{email_oss::send_email_html, server::load_smtp_config};
use windmill_object_store::upload_artifact_to_store;
#[cfg(feature = "inline_preview")]
use windmill_parser::asset_parser::AssetKind;
use windmill_types::s3::BundleFormat;
#[cfg(feature = "inline_preview")]
use windmill_worker::get_worker_internal_server_inline_utils;
@@ -1386,7 +1386,8 @@ async fn get_logs_from_store(
log_file_index: &Option<Vec<String>>,
) -> Option<error::Result<Body>> {
use futures::StreamExt;
let stream = windmill_object_store::get_logs_from_store(log_offset, logs, log_file_index).await?;
let stream =
windmill_object_store::get_logs_from_store(log_offset, logs, log_file_index).await?;
let header = bytes::Bytes::from(
r#"to remove ansi colors, use: | sed 's/\x1B\[[0-9;]\{1,\}[A-Za-z]//g'
"#
@@ -2849,6 +2850,7 @@ struct Preview {
dedicated_worker: Option<bool>,
lock: Option<String>,
format: Option<String>,
flow_path: Option<String>,
}
#[cfg(feature = "inline_preview")]
@@ -4509,6 +4511,14 @@ async fn run_preview_script(
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into());
let preview_args = preview.args.unwrap_or_default();
let flow_path_extra = preview.flow_path.map(|fp| {
let mut extra = HashMap::new();
extra.insert("_FLOW_PATH".to_string(), to_raw_value(&fp));
extra
});
let push_args = PushArgs { extra: flow_path_extra, args: &preview_args };
let (uuid, tx) = push(
&db,
tx,
@@ -4532,7 +4542,7 @@ async fn run_preview_script(
dedicated_worker: preview.dedicated_worker,
}),
},
PushArgs::from(&preview.args.unwrap_or_default()),
push_args,
authed.display_username(),
&authed.email,
username_to_permissioned_as(&authed.username),
@@ -5772,7 +5782,9 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = windmill_object_store::get_object_store().await {
let file = os
.get(&windmill_object_store::object_store_reexports::Path::from(format!("logs/{file_p}")))
.get(&windmill_object_store::object_store_reexports::Path::from(
format!("logs/{file_p}"),
))
.await;
if let Ok(file) = file {
if let Ok(bytes) = file.bytes().await {

View File

@@ -10,6 +10,7 @@ pub const REQUEST_SIZE_LIMIT_SETTING: &str = "request_size_limit_mb";
pub const LICENSE_KEY_SETTING: &str = "license_key";
pub const NPM_CONFIG_REGISTRY_SETTING: &str = "npm_config_registry";
pub const BUNFIG_INSTALL_SCOPES_SETTING: &str = "bunfig_install_scopes";
pub const NPMRC_SETTING: &str = "npmrc";
pub const NUGET_CONFIG_SETTING: &str = "nuget_config";
pub const POWERSHELL_REPO_URL_SETTING: &str = "powershell_repo_url";
pub const POWERSHELL_REPO_PAT_SETTING: &str = "powershell_repo_pat";

View File

@@ -261,6 +261,8 @@ pub struct GlobalSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub bunfig_install_scopes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub npmrc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nuget_config: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maven_repos: Option<String>,
@@ -774,7 +776,11 @@ pub const PROTECTED_SETTINGS: &[&str] = &[
/// Note: jwt_secret is intentionally NOT hidden — it is included in YAML exports so that
/// operators can set it via ConfigMap. It is protected from deletion (PROTECTED_SETTINGS)
/// and from being set to empty/null, and its value is partially redacted in log output.
pub const HIDDEN_SETTINGS: &[&str] = &["uid", "min_keep_alive_version", "automate_username_creation"];
pub const HIDDEN_SETTINGS: &[&str] = &[
"uid",
"min_keep_alive_version",
"automate_username_creation",
];
/// Top-level settings whose entire value is sensitive and must be fully redacted in logs.
const SENSITIVE_SETTINGS: &[&str] = &[
@@ -788,6 +794,7 @@ const SENSITIVE_SETTINGS: &[&str] = &[
"pip_extra_index_url",
"npm_config_registry",
"bunfig_install_scopes",
"npmrc",
"maven_repos",
"ruby_repos",
"powershell_repo_pat",
@@ -798,7 +805,10 @@ const SENSITIVE_SETTINGS: &[&str] = &[
const NESTED_SENSITIVE_FIELDS: &[(&str, &[&str])] = &[
("smtp_settings", &["smtp_password"]),
("secret_backend", &["token"]),
("object_store_cache_config", &["secret_key", "serviceAccountKey"]),
(
"object_store_cache_config",
&["secret_key", "serviceAccountKey"],
),
];
fn redact_json_value(value: &serde_json::Value) -> serde_json::Value {
@@ -2353,7 +2363,11 @@ mod tests {
);
let diff = diff_global_settings(&current, &desired, ApplyMode::Merge);
assert_eq!(diff.upserts.len(), 1, "Same client with newer expiry should update even with different signature");
assert_eq!(
diff.upserts.len(),
1,
"Same client with newer expiry should update even with different signature"
);
}
#[test]

View File

@@ -1281,3 +1281,96 @@ mod tests {
assert_eq!(parsed, serde_json::json!([[1], [2], [3], [4], [5]]));
}
}
/// Parse .npmrc content to extract the default registry URL and its auth token.
/// Returns `Some((registry_url, Option<auth_token>))` if a default registry is found.
pub fn parse_npmrc_registry(npmrc_content: &str) -> Option<(String, Option<String>)> {
let mut registry_url: Option<String> = None;
let mut auth_tokens: Vec<(String, String)> = Vec::new();
for line in npmrc_content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
continue;
}
if let Some(url) = line.strip_prefix("registry=") {
registry_url = Some(url.trim().to_string());
}
if line.starts_with("//") {
if let Some((prefix, token)) = line.split_once(":_authToken=") {
auth_tokens.push((prefix.to_string(), token.to_string()));
}
}
}
let url = registry_url?;
let url_without_protocol = url.trim_start_matches("https:").trim_start_matches("http:");
let url_prefix = url_without_protocol.trim_end_matches('/');
let token = auth_tokens
.iter()
.find(|(prefix, _)| {
let p = prefix.trim_end_matches('/');
p == url_prefix
})
.map(|(_, token)| token.clone());
Some((url, token))
}
#[cfg(test)]
mod npmrc_tests {
use super::parse_npmrc_registry;
#[test]
fn test_parse_simple_registry() {
let npmrc = "registry=https://registry.mycompany.com/\n//registry.mycompany.com/:_authToken=secret123\n";
let result = parse_npmrc_registry(npmrc);
assert_eq!(
result,
Some((
"https://registry.mycompany.com/".to_string(),
Some("secret123".to_string())
))
);
}
#[test]
fn test_parse_registry_without_auth() {
let npmrc = "registry=https://registry.npmjs.org/\n";
let result = parse_npmrc_registry(npmrc);
assert_eq!(
result,
Some(("https://registry.npmjs.org/".to_string(), None))
);
}
#[test]
fn test_parse_scoped_only_no_default() {
let npmrc =
"@myorg:registry=https://registry.myorg.com/\n//registry.myorg.com/:_authToken=tok\n";
let result = parse_npmrc_registry(npmrc);
assert_eq!(result, None);
}
#[test]
fn test_parse_with_comments() {
let npmrc = "# My registry\nregistry=https://r.example.com/\n; auth\n//r.example.com/:_authToken=tok\n";
let result = parse_npmrc_registry(npmrc);
assert_eq!(
result,
Some((
"https://r.example.com/".to_string(),
Some("tok".to_string())
))
);
}
#[test]
fn test_parse_empty_npmrc() {
assert_eq!(parse_npmrc_registry(""), None);
assert_eq!(parse_npmrc_registry("# just a comment"), None);
}
}

View File

@@ -427,7 +427,7 @@ fn format_pull_query(peek: String) -> String {
j.same_worker, j.pre_run_error, j.visible_to_owner,
j.tag, j.concurrent_limit, j.concurrency_time_window_s, j.flow_innermost_root_job, j.root_job,
j.timeout, j.flow_step_id, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle, j.priority, j.raw_code, j.raw_lock, j.raw_flow,
j.script_entrypoint_override, j.preprocessed, pj.runnable_path as parent_runnable_path,
j.script_entrypoint_override, j.preprocessed, COALESCE(pj.runnable_path, j.args->>'_FLOW_PATH') as parent_runnable_path,
COALESCE(p.email, j.permissioned_as_email) as permissioned_as_email, p.username as permissioned_as_username, p.is_admin as permissioned_as_is_admin,
p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders, p.end_user_email as permissioned_as_end_user_email
FROM q, j

View File

@@ -5321,7 +5321,11 @@ async fn push_inner<'c, 'd>(
.as_ref()
.map(|x| {
let tag_lang = if x == &ScriptLang::Bunnative {
ScriptLang::Nativets.as_str()
if job_kind == JobKind::Dependencies {
ScriptLang::Bun.as_str()
} else {
ScriptLang::Nativets.as_str()
}
} else {
x.as_str()
};
@@ -5364,7 +5368,6 @@ async fn push_inner<'c, 'd>(
job_id,
&args,
&mut tx,
_db,
)
.await?
}
@@ -6204,7 +6207,7 @@ pub async fn get_same_worker_job(
v2_job.raw_code,
v2_job.raw_lock,
v2_job.raw_flow,
pj.runnable_path as parent_runnable_path,
COALESCE(pj.runnable_path, v2_job.args->>'_FLOW_PATH') as parent_runnable_path,
p.email as permissioned_as_email, p.username as permissioned_as_username, p.is_admin as permissioned_as_is_admin,
p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders, p.end_user_email as permissioned_as_end_user_email
FROM v2_job_queue

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,8 @@ use crate::{
handle_child::handle_child,
is_sandboxing_enabled, read_ee_registry, BUNFIG_INSTALL_SCOPES, BUN_BUNDLE_CACHE_DIR,
BUN_CACHE_DIR, BUN_NO_CACHE, BUN_PATH, DISABLE_NUSER, HOME_ENV, NODE_BIN_PATH, NODE_PATH,
NPM_CONFIG_REGISTRY, NPM_PATH, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, TRACING_PROXY_CA_CERT_PATH,
TZ_ENV,
NPMRC, NPM_CONFIG_REGISTRY, NPM_PATH, NSJAIL_PATH, PATH_ENV, PROXY_ENVS,
TRACING_PROXY_CA_CERT_PATH, TZ_ENV,
};
use windmill_common::{
client::AuthedClient,
@@ -299,6 +299,20 @@ async fn gen_bunfig(
w_id: &str,
db: Option<&Connection>,
) -> Result<()> {
let npmrc = if let Some(conn) = db {
read_ee_registry(NPMRC.read().await.clone(), "npmrc", job_id, w_id, conn).await
} else {
NPMRC.read().await.clone()
};
if let Some(ref npmrc_content) = npmrc {
if !npmrc_content.trim().is_empty() {
tracing::debug!("Writing .npmrc for bun from npmrc setting");
write_file(job_dir, ".npmrc", npmrc_content)?;
return Ok(());
}
}
let (registry, bunfig_install_scopes) = if let Some(conn) = db {
(
read_ee_registry(
@@ -402,39 +416,55 @@ pub async fn install_bun_lockfile(
};
let has_file = if npm_mode {
let registry = if let Some(conn) = db {
read_ee_registry(
NPM_CONFIG_REGISTRY.read().await.clone(),
"npm registry",
job_id,
w_id,
conn,
)
.await
let npmrc = if let Some(conn) = db {
read_ee_registry(NPMRC.read().await.clone(), "npmrc", job_id, w_id, conn).await
} else {
NPM_CONFIG_REGISTRY.read().await.clone()
NPMRC.read().await.clone()
};
if let Some(registry) = registry {
let content = registry
.trim_start_matches("https:")
.trim_start_matches("http:");
let mut splitted = registry.split(":_authToken=");
let custom_registry = splitted.next().unwrap_or_default();
npm_logs.push_str(&format!(
"Using custom npm registry: {custom_registry} {}\n",
if splitted.next().is_some() {
"with authToken"
} else {
"without authToken"
}
));
child_cmd.env("NPM_CONFIG_REGISTRY", custom_registry);
write_file(job_dir, ".npmrc", content)?;
true
if let Some(ref npmrc_content) = npmrc {
if !npmrc_content.trim().is_empty() {
npm_logs.push_str("Using .npmrc from instance settings\n");
write_file(job_dir, ".npmrc", npmrc_content)?;
true
} else {
false
}
} else {
false
let registry = if let Some(conn) = db {
read_ee_registry(
NPM_CONFIG_REGISTRY.read().await.clone(),
"npm registry",
job_id,
w_id,
conn,
)
.await
} else {
NPM_CONFIG_REGISTRY.read().await.clone()
};
if let Some(registry) = registry {
let content = registry
.trim_start_matches("https:")
.trim_start_matches("http:");
let mut splitted = registry.split(":_authToken=");
let custom_registry = splitted.next().unwrap_or_default();
npm_logs.push_str(&format!(
"Using custom npm registry: {custom_registry} {}\n",
if splitted.next().is_some() {
"with authToken"
} else {
"without authToken"
}
));
child_cmd.env("NPM_CONFIG_REGISTRY", custom_registry);
write_file(job_dir, ".npmrc", content)?;
true
} else {
false
}
}
} else {
false
@@ -446,9 +476,11 @@ pub async fn install_bun_lockfile(
}
}
let mut child_process = start_child_process(child_cmd, &*BUN_PATH, false).await?;
if !has_file {
gen_bunfig(job_dir, job_id, w_id, db).await?;
}
gen_bunfig(job_dir, job_id, w_id, db).await?;
let mut child_process = start_child_process(child_cmd, &*BUN_PATH, false).await?;
if let Some(db) = db {
handle_child(
job_id,

View File

@@ -13,7 +13,7 @@ use crate::{
},
get_proxy_envs_for_lang,
handle_child::handle_child,
is_sandboxing_enabled, read_ee_registry, DENO_CACHE_DIR, DENO_PATH, HOME_ENV,
is_sandboxing_enabled, read_ee_registry, DENO_CACHE_DIR, DENO_PATH, HOME_ENV, NPMRC,
NPM_CONFIG_REGISTRY, PATH_ENV, TZ_ENV,
};
use windmill_common::client::AuthedClient;
@@ -79,21 +79,29 @@ async fn get_common_deno_proc_envs(
),
]);
let registry = if let Some(conn) = conn {
read_ee_registry(
NPM_CONFIG_REGISTRY.read().await.clone(),
"npm registry",
job_id,
w_id,
conn,
)
.await
let npmrc = if let Some(conn) = conn {
read_ee_registry(NPMRC.read().await.clone(), "npmrc", job_id, w_id, conn).await
} else {
NPM_CONFIG_REGISTRY.read().await.clone()
NPMRC.read().await.clone()
};
if let Some(ref s) = registry {
let (url, _token_opt) = parse_npm_config(s);
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), url);
if npmrc.as_ref().map_or(true, |s| s.trim().is_empty()) {
let registry = if let Some(conn) = conn {
read_ee_registry(
NPM_CONFIG_REGISTRY.read().await.clone(),
"npm registry",
job_id,
w_id,
conn,
)
.await
} else {
NPM_CONFIG_REGISTRY.read().await.clone()
};
if let Some(ref s) = registry {
let (url, _token_opt) = parse_npm_config(s);
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), url);
}
}
if DENO_CERT.len() > 0 {
deno_envs.insert(String::from("DENO_CERT"), DENO_CERT.clone());
@@ -390,6 +398,21 @@ try {{
common_deno_proc_envs.insert("HOME".to_string(), job_dir.to_string());
}
let npmrc = read_ee_registry(
NPMRC.read().await.clone(),
"npmrc",
&job.id,
&job.workspace_id,
conn,
)
.await;
if let Some(ref npmrc_content) = npmrc {
if !npmrc_content.trim().is_empty() {
write_file(job_dir, ".npmrc", npmrc_content)?;
write_file(job_dir, "deno.json", "{}")?;
}
}
//do not cache local dependencies
let child = {
let reload = format!("--reload={base_internal_url}");

View File

@@ -11,7 +11,6 @@ use tiberius::{
use tokio::net::TcpStream;
use tokio_util::compat::TokioAsyncWriteCompatExt;
use uuid::Uuid;
use windmill_object_store::convert_json_line_stream;
use windmill_common::utils::merge_raw_values_to_object;
use windmill_common::worker::SqlResultCollectionStrategy;
use windmill_common::{
@@ -19,6 +18,7 @@ use windmill_common::{
utils::empty_as_none,
worker::{to_raw_value, Connection},
};
use windmill_object_store::convert_json_line_stream;
use windmill_parser_sql::{parse_db_resource, parse_mssql_sig, parse_s3_mode};
use windmill_queue::MiniPulledJob;
use windmill_queue::{append_logs, CanceledBy};
@@ -428,9 +428,7 @@ fn sql_to_json_value(val: ColumnData) -> Result<Box<RawValue>, Error> {
}
fn numeric_to_raw_value(numeric: &tiberius::numeric::Numeric) -> Result<Box<RawValue>, Error> {
// tiberius::Numeric::to_string is broken, don't use it
let sign = if numeric.int_part().is_negative() {
let sign = if numeric.value().is_negative() {
"-"
} else {
""
@@ -468,6 +466,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use tiberius::numeric::Numeric;
#[test]
fn test_sql_to_json_value_numeric_null() {
@@ -477,7 +476,6 @@ mod tests {
#[test]
fn test_sql_to_json_value_numeric_integer() {
use tiberius::numeric::Numeric;
let numeric = Numeric::new_with_scale(12345, 0);
let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap();
assert_eq!(result.get(), "12345");
@@ -485,7 +483,6 @@ mod tests {
#[test]
fn test_sql_to_json_value_numeric_decimal() {
use tiberius::numeric::Numeric;
let numeric = Numeric::new_with_scale(123456, 2); // Represents 1234.56
let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap();
assert_eq!(result.get(), "1234.56");
@@ -493,7 +490,6 @@ mod tests {
#[test]
fn test_sql_to_json_value_numeric_negative() {
use tiberius::numeric::Numeric;
let numeric = Numeric::new_with_scale(-98765, 2); // Represents -987.65
let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap();
assert_eq!(result.get(), "-987.65");
@@ -501,7 +497,6 @@ mod tests {
#[test]
fn test_sql_to_json_value_numeric_negative_integer() {
use tiberius::numeric::Numeric;
let numeric = Numeric::new_with_scale(-98765, 0);
let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap();
assert_eq!(result.get(), "-98765");
@@ -509,15 +504,21 @@ mod tests {
#[test]
fn test_sql_to_json_value_numeric_high_precision() {
use tiberius::numeric::Numeric;
let numeric = Numeric::new_with_scale(123456789012345, 10); // High precision
let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap();
assert_eq!(result.get(), "12345.6789012345");
}
#[test]
fn test_sql_to_json_value_numeric_negative_fractional_only() {
// -0.4: int_part() is 0, so old code lost the negative sign
let numeric = Numeric::new_with_scale(-4, 1);
let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap();
assert_eq!(result.get(), "-0.4");
}
#[test]
fn test_sql_to_json_value_numeric_7_69() {
use tiberius::numeric::Numeric;
let numeric = Numeric::new_with_scale(769, 2);
let result = sql_to_json_value(ColumnData::Numeric(Some(numeric))).unwrap();
assert_eq!(result.get(), "7.69");

View File

@@ -571,6 +571,7 @@ lazy_static::lazy_static! {
pub static ref NPM_CONFIG_REGISTRY: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
pub static ref BUNFIG_INSTALL_SCOPES: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
pub static ref NPMRC: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
pub static ref BUN_NO_CACHE: bool = std::env::var("BUN_NO_CACHE")
.ok()
.and_then(|x| x.parse::<bool>().ok())

View File

@@ -1328,29 +1328,30 @@ pub async fn update_flow_status_after_job_completion_internal(
if module_step.is_preprocessor_step() && success {
let tag_and_concurrency_key = get_tag_and_concurrency(&flow, db).await;
let require_args = tag_and_concurrency_key.as_ref().is_some_and(|x| {
let has_debouncing = flow_value
.debouncing_settings
.debounce_delay_s
.filter(|x| *x > 0)
.is_some();
let concurrency_requires_args = tag_and_concurrency_key.as_ref().is_some_and(|x| {
x.tag.as_ref().is_some_and(|t| t.contains("$args"))
|| x.concurrency_key
.as_ref()
.is_some_and(|ck| ck.contains("$args"))
});
let mut tag = tag_and_concurrency_key
.as_ref()
.map(|x| x.tag.clone())
.flatten();
let require_args = concurrency_requires_args || has_debouncing;
let mut tag = tag_and_concurrency_key.as_ref().and_then(|x| x.tag.clone());
let concurrency_key = tag_and_concurrency_key
.as_ref()
.map(|x| x.concurrency_key.clone())
.flatten();
.and_then(|x| x.concurrency_key.clone());
let concurrent_limit = tag_and_concurrency_key
.as_ref()
.map(|x| x.concurrent_limit)
.flatten();
.and_then(|x| x.concurrent_limit);
let concurrency_time_window_s = tag_and_concurrency_key
.as_ref()
.map(|x| x.concurrency_time_window_s)
.flatten();
if require_args {
.and_then(|x| x.concurrency_time_window_s);
let fetched_args = if require_args {
let args = sqlx::query_scalar!(
"SELECT result as \"result: Json<HashMap<String, Box<RawValue>>>\"
FROM v2_job_completed
@@ -1362,8 +1363,13 @@ pub async fn update_flow_status_after_job_completion_internal(
.map_err(|e| {
Error::internal_err(format!("error while fetching preprocessing args: {e:#}"))
})?;
let args_hm = args.unwrap_or_default().0;
let args = PushArgs::from(&args_hm);
Some(args.unwrap_or_default().0)
} else {
None
};
if concurrency_requires_args {
let args = PushArgs::from(fetched_args.as_ref().unwrap());
if let Some(ck) = concurrency_key {
insert_concurrency_key(
&flow_job.workspace_id,
@@ -1392,8 +1398,31 @@ pub async fn update_flow_status_after_job_completion_internal(
.await?;
}
// let tag = tag_and_concurrency_key.and_then(|tc| tc.tag.map(|t| interpolate_args(t.clone(), &args, &workspace_id)));
// let concurrency_key = tag_and_concurrency_key.and_then(|tc| tc.concurrency_key.map(|ck| interpolate_args(&ck, &args, &workspace_id)));
let scheduled_for: Option<chrono::DateTime<chrono::Utc>> = {
#[cfg(feature = "private")]
{
if has_debouncing {
let empty_hm = HashMap::new();
let args = PushArgs::from(fetched_args.as_ref().unwrap_or(&empty_hm));
windmill_queue::jobs_ee::maybe_debounce_post_preprocessing(
&flow_value.debouncing_settings,
&flow_job.runnable_path,
&flow_job.workspace_id,
flow,
&args,
db,
)
.await?
} else {
None
}
}
#[cfg(not(feature = "private"))]
{
None
}
};
sqlx::query!(
"WITH job_result AS (
SELECT result
@@ -1403,7 +1432,8 @@ pub async fn update_flow_status_after_job_completion_internal(
updated_queue AS (
UPDATE v2_job_queue
SET running = false,
tag = COALESCE($3, tag)
tag = COALESCE($3, tag),
scheduled_for = COALESCE($6, scheduled_for)
WHERE id = $2
)
UPDATE v2_job
@@ -1431,6 +1461,7 @@ pub async fn update_flow_status_after_job_completion_internal(
tag,
concurrent_limit,
concurrency_time_window_s,
scheduled_for,
)
.execute(db)
.await

View File

@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.640.0";
export const VERSION = "v1.642.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

1
cli/.npmrc Normal file
View File

@@ -0,0 +1 @@
@jsr:registry=https://npm.jsr.io

83
cli/build-npm.ts Normal file
View File

@@ -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}/`);

View File

@@ -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!"

312
cli/bun.lock Normal file
View File

@@ -0,0 +1,312 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "windmill-cli-dev",
"dependencies": {
"@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",
"@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",
"tar-stream": "^3.1.7",
"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/tar-stream": "^3.1.4",
"@types/ws": "^8.5.0",
"typescript": "^5.7.0",
},
},
},
"packages": {
"@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__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=="],
"@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/tar-stream": ["@types/tar-stream@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@windmill-labs/shared-utils": ["@windmill-labs/shared-utils@1.0.12", "", {}, "sha512-n68uEYv2B5q2Pp8J9syMS3qPZbppFEfeM7HIBEUfU5lGqi3hwnv4mPvgRUyb6K9im3frXC4gzdIdZdlrDpudXQ=="],
"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=="],
"b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="],
"balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="],
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
"brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"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=="],
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"get-port": ["get-port@7.1.0", "", {}, "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw=="],
"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=="],
"set-immediate-shim": ["set-immediate-shim@1.0.1", "", {}, "sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ=="],
"streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="],
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"svelte": ["svelte@5.53.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-WzxFHZhhD23Qzu7JCYdvm1rxvRSzdt9HtHO8TScMBX51bLRFTcJmATVqjqXG+6Ln6hrViGCo9DzwOhAasxwC/w=="],
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
"text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"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=="],
}
}

4
cli/bunfig.toml Normal file
View File

@@ -0,0 +1,4 @@
[test]
preload = ["./test/setup.ts"]
timeout = 60000
root = "./test"

View File

@@ -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"
}

1806
cli/deno.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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);
};

View File

@@ -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");
},
});

View File

@@ -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 <<EOF - gen/core/OpenAPI.ts > 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";

View File

@@ -2,14 +2,55 @@
set -e
if [ -z "$1" ]; then
name="wmill"
else
name="$1"
# Parse options
USE_NODE=false
name=""
for arg in "$@"; do
case "$arg" in
--node|-node|---node) USE_NODE=true ;;
-*) echo "Unknown option: $arg"; echo "Usage: $0 [name] [--node]"; exit 1 ;;
*) [ -z "$name" ] && name="$arg" ;;
esac
done
if [ -z "$name" ]; then
name="wmill-dev"
fi
./gen_wm_client.sh
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
./gen_wm_client.sh
./windmill-utils-internal/gen_wm_client.sh
echo "Installing dev cli as $name (pass arg to override)"
deno install -f -A -g src/main.ts --name $name --unstable
bun install
INSTALL_DIR="$HOME/.local/bin"
mkdir -p "$INSTALL_DIR"
if [ "$USE_NODE" = true ]; then
echo "Building npm bundle..."
bun run build-npm.ts
NPM_DIR="$SCRIPT_DIR/npm"
cd "$NPM_DIR" && npm install
cd "$SCRIPT_DIR"
cat > "$INSTALL_DIR/$name" <<EOF
#!/bin/sh
exec node "$NPM_DIR/esm/main.js" "\$@"
EOF
else
cat > "$INSTALL_DIR/$name" <<EOF
#!/bin/sh
exec bun run "$SCRIPT_DIR/src/main.ts" "\$@"
EOF
fi
chmod +x "$INSTALL_DIR/$name"
echo "Installed dev cli as '$name' at $INSTALL_DIR/$name"
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
echo "Warning: $INSTALL_DIR is not in your PATH. Add it with:"
echo " export PATH=\"$INSTALL_DIR:\$PATH\""
fi

1498
cli/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
cli/package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"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": {
"@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",
"@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",
"tar-stream": "^3.1.7",
"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/tar-stream": "^3.1.4",
"@types/ws": "^8.5.0",
"typescript": "^5.7.0"
}
}

View File

@@ -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 "../../core/log.ts";
import { sep as SEP } from "node:path";
import * as windmillUtils from "@windmill-labs/shared-utils";
import { yamlParseFile } from "../../utils/yaml.ts";
import * as wmill from "../../../gen/services.gen.ts";
import { ListableApp, Policy } from "../../../gen/types.gen.ts";
@@ -188,7 +185,7 @@ export async function generatingPolicy(
}
}
async function list(opts: GlobalOptions & { includeDraftOnly?: boolean }) {
async function list(opts: GlobalOptions & { includeDraftOnly?: boolean; json?: boolean }) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
@@ -209,12 +206,32 @@ async function list(opts: GlobalOptions & { includeDraftOnly?: boolean }) {
}
}
new Table()
.header(["path", "summary"])
.padding(2)
.border(true)
.body(total.map((x) => [x.path, x.summary]))
.render();
if (opts.json) {
console.log(JSON.stringify(total));
} else {
new Table()
.header(["path", "summary"])
.padding(2)
.border(true)
.body(total.map((x) => [x.path, x.summary]))
.render();
}
}
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
const a = await wmill.getAppByPath({
workspace: workspace.workspaceId,
path,
});
if (opts.json) {
console.log(JSON.stringify(a));
} else {
console.log(colors.bold("Path:") + " " + a.path);
console.log(colors.bold("Summary:") + " " + (a.summary ?? ""));
console.log(colors.bold("Created by:") + " " + (a.created_by ?? ""));
}
}
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
@@ -230,7 +247,15 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
const command = new Command()
.description("app related commands")
.option("--json", "Output as JSON (for piping to jq)")
.action(list as any)
.command("list", "list all apps")
.option("--json", "Output as JSON (for piping to jq)")
.action(list as any)
.command("get", "get an app's details")
.arguments("<path:string>")
.option("--json", "Output as JSON (for piping to jq)")
.action(get as any)
.command("push", "push a local app ")
.arguments("<file_path:string> <remote_path:string>")
.action(push as any)

View File

@@ -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 "../../core/log.ts";
import { sep as SEP } from "node:path";
import { yamlParseFile } from "../../utils/yaml.ts";
import { stringify as yamlStringify } from "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) ||

View File

@@ -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 "../../core/log.ts";
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<any[]> {
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<BundleResult> {
// 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());

View File

@@ -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 "../../core/log.ts";
import { sep as SEP } from "node:path";
import * as windmillUtils from "@windmill-labs/shared-utils";
import { yamlParseFile } from "../../utils/yaml.ts";
import * as getPort from "get-port";
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<string, ReturnType<typeof setTimeout>> = {};
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<void> {
@@ -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<string, ReturnType<typeof setTimeout>> = {};
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(() => {

View File

@@ -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 "../../core/log.ts";
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]")

View File

@@ -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 "../../core/log.ts";
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"));

View File

@@ -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 "../../core/log.ts";
import { stringify as yamlStringify } from "yaml";
import { GlobalOptions } from "../../types.ts";
import { generateAgentsDocumentation, generateDatatablesDocumentation, yamlOptions } from "../sync/sync.ts";
import { resolveWorkspace } from "../../core/context.ts";
@@ -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<string, unknown> = {
@@ -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<string> {
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);

View File

@@ -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 "../../core/log.ts";
import { sep as SEP } from "node:path";
import * as windmillUtils from "@windmill-labs/shared-utils";
import { yamlParseFile } from "../../utils/yaml.ts";
import { stringify as yamlStringify } from "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<string, string> = {};
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;
}
}

View File

@@ -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 "../../core/log.ts";
import * as wmill from "../../../gen/services.gen.ts";
import fs from "node:fs";
import { workspaceDependenciesPathToLanguageAndFilename } from "../../utils/metadata.ts";

View File

@@ -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 "../../core/log.ts";
import { sep as SEP } from "node: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<string, number> = {};
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<string, ReturnType<typeof setTimeout>> = {};
function watchChanges() {
return new Promise<void>((_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 <pattern...:string>",
"Filter paths givena glob pattern or path"
)
// deno-lint-ignore no-explicit-any
.action(dev as any);
export default command;

Some files were not shown because too many files have changed in this diff Show More