Compare commits

..

2 Commits

Author SHA1 Message Date
centdix
cf7ca75db0 test: add intentional compilation error to fail backend CI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 12:45:30 +01:00
centdix
c80d9a4505 config 2026-03-13 12:10:00 +01:00
83 changed files with 890 additions and 4303 deletions

View File

@@ -13,10 +13,8 @@ fi
# Check if the file is in the backend directory and is a Rust file
if [[ "$FILE_PATH" == *"/backend/"* ]] && [[ "$FILE_PATH" =~ \.rs$ ]]; then
cd "$CLAUDE_PROJECT_DIR/backend" || exit 0
# Run rustfmt, surface errors as context but don't block Claude
if rustfmt --config-path rustfmt.toml "$FILE_PATH" 2>&1; then
echo "Formatted $(basename "$FILE_PATH")"
fi
# Run rustfmt with config from rustfmt.toml (edition=2021)
rustfmt --config-path rustfmt.toml "$FILE_PATH" 2>/dev/null || true
fi
exit 0

View File

@@ -15,10 +15,8 @@ if [[ "$FILE_PATH" == *"/frontend/"* ]]; then
# Check if it's a formattable file type
if [[ "$FILE_PATH" =~ \.(ts|js|svelte|json|css|html|md)$ ]]; then
cd "$CLAUDE_PROJECT_DIR/frontend" || exit 0
# Run prettier, surface errors as context but don't block Claude
if ./node_modules/.bin/prettier --plugin prettier-plugin-svelte --write "$FILE_PATH" 2>&1; then
echo "Formatted $(basename "$FILE_PATH")"
fi
# Run prettier silently, don't fail the hook if prettier fails
npx prettier --write "$FILE_PATH" 2>/dev/null || true
fi
fi

View File

@@ -28,12 +28,6 @@
"Bash(git show:*)",
"Bash(git blame:*)",
"Bash(cargo check:*)",
"Bash(cargo build --release:*)",
"Bash(sh wm-ts-nav/nav:*)",
"Bash(wm-ts-nav/nav:*)",
"Bash(./wm-ts-nav/nav:*)",
"Bash(wm-ts-nav/target/release/wm-ts-nav:*)",
"Bash(./wm-ts-nav/target/release/wm-ts-nav:*)",
"mcp__ide__getDiagnostics",
"Bash(npm run generate-backend-client:*)",
"Bash(npm run check:*)",

View File

@@ -1,98 +0,0 @@
---
name: local-review
user_invocable: true
description: Code review a pull request for bugs and CLAUDE.md compliance. MUST use when asked to review code.
---
# Local Code Review Skill
Review a pull request for real bugs and CLAUDE.md compliance violations. This review targets HIGH SIGNAL issues only.
## Review Philosophy
- **Only flag issues you are certain about.** If you are not sure an issue is real, do not flag it. False positives erode trust and waste reviewer time.
- Think like a senior engineer doing a final review — flag things that would cause incidents, not things that are merely imperfect.
## What to Flag
- Code that won't compile or parse (syntax errors, type errors, missing imports)
- Code that will definitely produce wrong results regardless of inputs
- Clear, unambiguous CLAUDE.md violations (quote the exact rule being violated)
- Security issues in introduced code (injection, auth bypass, data exposure)
- Incorrect logic that will fail in production
## What NOT to Flag
- Code style or quality concerns
- Potential issues that depend on specific inputs or runtime state
- Subjective suggestions or improvements
- Pre-existing issues not introduced by this PR
- Pedantic nitpicks a senior engineer wouldn't flag
- Issues a linter or type checker will catch
- General quality concerns unless explicitly prohibited in CLAUDE.md
- Issues silenced via lint ignore comments
## Execution Steps
1. **Determine the PR scope**:
- If an argument is provided, use it as the PR number or branch
- Otherwise, detect from the current branch vs main
- Run `gh pr view` if a PR exists, or use `git diff main...HEAD`
2. **Find relevant CLAUDE.md files**:
- Read the root `CLAUDE.md`
- Check for CLAUDE.md files in directories containing changed files
3. **Get the diff and metadata**:
- `gh pr diff` or `git diff main...HEAD` for the full diff
- `gh pr view` or `git log main..HEAD --oneline` for context
4. **Read changed files** where the diff alone is insufficient to understand context
5. **Review for**:
- CLAUDE.md compliance — check each rule against the changed code
- Bugs and logic errors — will this code work correctly?
- Security issues — injection, auth, data exposure in new code
6. **Self-validate each finding**: Before reporting, ask yourself:
- "Is this definitely a real issue, not a false positive?"
- "Would a senior engineer flag this in review?"
- If the answer to either is no, discard the finding
7. **Output findings** to the terminal (default) or post as PR comments (with `--comment` flag)
## Output Format
```
## Code review
Found N issues:
1. <description> (<reason: CLAUDE.md adherence | bug | security>)
<file_path:line_number>
2. <description> (<reason>)
<file_path:line_number>
```
If no issues are found:
```
## Code review
No issues found. Checked for bugs and CLAUDE.md compliance.
```
## Posting Comments (--comment flag)
If the user passes `--comment`, post findings as inline PR comments using:
```bash
gh pr review --comment --body "<summary>"
```
Or for inline comments on specific lines:
```bash
gh api repos/{owner}/{repo}/pulls/{pr}/reviews -f body="<summary>" -f event="COMMENT" -f comments="[...]"
```

View File

@@ -33,7 +33,6 @@ Follow conventional commit format for the PR title:
- Keep under 70 characters
- Use lowercase, imperative mood
- No period at the end
- If `*_ee.rs` files were modified, prefix with `[ee]`: `[ee] <type>: <description>`
## PR Body Format
@@ -86,25 +85,3 @@ Generated with [Claude Code](https://claude.com/claude-code)
)"
```
7. Return the PR URL to the user
## EE Companion PR (when `*_ee.rs` files were modified)
The `*_ee.rs` files in the windmill repo are **symlinks** to `windmill-ee-private` — changes won't appear in `git diff` of the windmill repo. Instead, check the EE repo for uncommitted or unpushed changes.
Follow the full EE PR workflow in `docs/enterprise.md`. The key PR-specific details:
1. Find the EE repo/worktree: see "Finding the EE Repo" in `docs/enterprise.md`
2. Check for changes: `git -C <ee-path> status --short`
- If there are no changes in the EE repo, skip this entire section
3. Follow steps 15 from the "EE PR Workflow" in `docs/enterprise.md`
4. Create the companion PR (title does NOT get the `[ee]` prefix):
```bash
gh pr create --draft --repo windmill-labs/windmill-ee-private --title "<type>: <description>" --body "$(cat <<'EOF'
Companion PR for windmill-labs/windmill#<PR_NUMBER>
---
Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```
5. Commit `ee-repo-ref.txt` and push the updated windmill branch

View File

@@ -14,7 +14,7 @@ jobs:
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
- run: cd typescript-client && ./publish.sh --access public && cd ..
- run: cd typescript-client && ./publish.sh && cd ..
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish_cli:
@@ -28,6 +28,6 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: cd cli && ./build.sh && cd npm && npm publish --access public
- run: cd cli && ./build.sh && cd npm && npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -9,7 +9,6 @@ workspace:
startupEnvs:
CARGO_FEATURES: "quickjs"
WM_CLONE_DB: false
USE_RUST_PLUGIN: false
lifecycleHooks:
postCreate: bash ./scripts/post-create.sh
@@ -61,29 +60,6 @@ profiles:
split: bottom
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/frontend" && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0
frontendOnly:
runtime: host
yolo: true
envPassthrough: []
systemPrompt: >
You are running inside a tmux session with other panes running services.
Pane layout (current window):
- Pane 0: this pane (claude agent)
- Pane 1: frontend (npm run dev)
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (frontend).
When restarting frontend, make sure to use ${FRONTEND_PORT}.
To connect to the database, use this connection string: ${DATABASE_URL}
Because we are running frontend with npm run dev, to verify your changes, just check the logs in the frontend pane. No need for npm run build.
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
panes:
- id: agent
kind: agent
focus: true
- id: frontend
kind: command
split: right
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/frontend" && npm run generate-backend-client && npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0
agentOnly:
runtime: host
yolo: true
@@ -96,10 +72,5 @@ profiles:
focus: true
integrations:
github:
linkedRepos:
- repo: windmill-labs/windmill-ee-private
alias: ee-private
dir: ../windmill-ee-private__worktrees
linear:
enabled: true

View File

@@ -67,7 +67,6 @@ files:
copy:
- backend/.env
- scripts/
- wm-ts-nav/target/release/wm-ts-nav
sandbox:
enabled: false

View File

@@ -1,50 +1,5 @@
# Changelog
## [1.657.2](https://github.com/windmill-labs/windmill/compare/v1.657.1...v1.657.2) (2026-03-15)
### Bug Fixes
* **cli:** Fix nonDottedPaths handling in cli flow lock generation ([#8375](https://github.com/windmill-labs/windmill/issues/8375)) ([eb03ebb](https://github.com/windmill-labs/windmill/commit/eb03ebbb0486b33c290fba3c34ea959e6e82fd13))
## [1.657.1](https://github.com/windmill-labs/windmill/compare/v1.657.0...v1.657.1) (2026-03-14)
### Bug Fixes
* powershell WindmillClient module loading on Windows workers ([#8370](https://github.com/windmill-labs/windmill/issues/8370)) ([3a268a9](https://github.com/windmill-labs/windmill/commit/3a268a9cf16add2ea2530e6eab247120a4d4754e))
## [1.657.0](https://github.com/windmill-labs/windmill/compare/v1.656.0...v1.657.0) (2026-03-14)
### Features
* add datatable config support to CLI settings sync and backend export ([#8024](https://github.com/windmill-labs/windmill/issues/8024)) ([5df37fb](https://github.com/windmill-labs/windmill/commit/5df37fb0dbf9190a430f066cf2d3c48914782e53))
## [1.656.0](https://github.com/windmill-labs/windmill/compare/v1.655.0...v1.656.0) (2026-03-13)
### Features
* add GitHub Enterprise Server (GHES) support for GitHub App git sync ([#8344](https://github.com/windmill-labs/windmill/issues/8344)) ([2e430c4](https://github.com/windmill-labs/windmill/commit/2e430c4c0b8540df7b6997434a7a9f9134858026))
* **cli:** add unified generate-metadata command ([#8335](https://github.com/windmill-labs/windmill/issues/8335)) ([4c2c165](https://github.com/windmill-labs/windmill/commit/4c2c165a5b757bd5f2f49074bb290407bce3b2fb))
### Bug Fixes
* **ci:** add NODE_AUTH_TOKEN for npm publish authentication ([2a8e276](https://github.com/windmill-labs/windmill/commit/2a8e276b6d2761bb2798b6bc5f8d90ab34fbb403))
* **ci:** remove provenance flag and use NPM_TOKEN for npm publish ([44dd3ee](https://github.com/windmill-labs/windmill/commit/44dd3ee8cd05d288828d1d46c84cbcdf40f8fa78))
* **cli:** exclude raw app backend files from script metadata generation ([#8362](https://github.com/windmill-labs/windmill/issues/8362)) ([060687b](https://github.com/windmill-labs/windmill/commit/060687b1fa6b627a7b06fbdc4b3f4eb0b63411c0))
* **cli:** normalize path separators in generate-metadata folder filter for Windows ([#8358](https://github.com/windmill-labs/windmill/issues/8358)) ([404ae09](https://github.com/windmill-labs/windmill/commit/404ae09d429fb545610ba17d747e1903c542d4a3))
* **cli:** suppress verbose lock generation messages in generate-metadata ([#8357](https://github.com/windmill-labs/windmill/issues/8357)) ([51933be](https://github.com/windmill-labs/windmill/commit/51933be3cabd853960d384cd358c7bcaef6bfa86))
* **frontend:** collapse flow topbar buttons to icon-only in narrow panes ([#8322](https://github.com/windmill-labs/windmill/issues/8322)) ([b585dee](https://github.com/windmill-labs/windmill/commit/b585dee64dfd63d20812ca969b17ff9ee9989493))
* **frontend:** filter webhook/email tokens by scope instead of label ([#8363](https://github.com/windmill-labs/windmill/issues/8363)) ([0d31c35](https://github.com/windmill-labs/windmill/commit/0d31c35f3e12d637c757a95fe350294002cbf640))
* **frontend:** improve native mode alert message and fix workspaced tag detection ([#8361](https://github.com/windmill-labs/windmill/issues/8361)) ([fb12b31](https://github.com/windmill-labs/windmill/commit/fb12b31df081b2f1ac63becea6e6538ca80f8c46))
* **frontend:** prevent duplicate and reserved agent tool names ([#8367](https://github.com/windmill-labs/windmill/issues/8367)) ([c431053](https://github.com/windmill-labs/windmill/commit/c431053a1e24ef29cd551a86de4d013fd7f158be))
* graceful shutdown instead of panic on job completion channel failure ([#8345](https://github.com/windmill-labs/windmill/issues/8345)) ([724d135](https://github.com/windmill-labs/windmill/commit/724d1350d070fcf078034a52166d3048fb74e6f3))
* Linked resources and vars not triggering both sync jobs on delete ([#8342](https://github.com/windmill-labs/windmill/issues/8342)) ([8e3b8bd](https://github.com/windmill-labs/windmill/commit/8e3b8bdfd2ded9652bc7e876c6bcd0ac2cfae148))
* lower default indexer memory/batch settings to prevent OOM ([#8347](https://github.com/windmill-labs/windmill/issues/8347)) ([d9d45cf](https://github.com/windmill-labs/windmill/commit/d9d45cf2f9235b0e7118d0fc97ccdc0776ca9726))
## [1.655.0](https://github.com/windmill-labs/windmill/compare/v1.654.0...v1.655.0) (2026-03-12)

View File

@@ -4,7 +4,7 @@ Open-source platform for internal tools, workflows, API integrations, background
## Workflow
1. **Understand**: Before coding, explore the codebase (see Code Navigation below). Use `outline` to understand file structure, `body` to read specific symbols, `def`/`callers`/`callees` to trace code, `Grep` to find usages. Read `docs/` for domain context.
1. **Understand**: Before coding, read relevant docs from `docs/` to understand the area you're changing
2. **Plan**: For non-trivial changes, use plan mode. For large features, break into reviewable stages
3. **Execute**: Follow coding patterns from skills (`rust-backend`, `svelte-frontend`)
4. **Validate**: After every change, run the appropriate checks per `docs/validation.md`
@@ -15,7 +15,6 @@ Open-source platform for internal tools, workflows, API integrations, background
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
- **Backend patterns**: use the `rust-backend` skill when writing Rust code
- **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code. Do NOT edit svelte files unless you have read that skill.
- **Code review**: use `/local-review` to review a PR for bugs and CLAUDE.md compliance
- **Domain guides**: `.claude/skills/native-trigger/` and `frontend/tutorial-system-guide.mdc`
- **Brand/UI guidelines**: `frontend/brand-guidelines.md`
@@ -50,35 +49,8 @@ let { my_prop = $bindable(default_value) }: { my_prop?: string } = $props()
2. **Create a `useMyPropState()` helper** — encapsulate the undefined-handling logic in a reusable function and call it higher in the component tree, so the child component always receives a defined value.
## Code Navigation
`wm-ts-nav` is an AST-aware code navigator. Use **wm-ts-nav** for structural queries — it skips comments/strings and understands symbol boundaries.
**MUST use `outline` before `Read`** on unfamiliar files — a 500-line file costs ~500 lines of context, while `outline` costs ~20. Then **MUST use `body "X"`** instead of reading a full file to see one function/struct. Use `Read` with offset/limit only when you need surrounding context that `body` doesn't capture.
- `refs "X" --caller` instead of reading files to find which function contains each reference
- `callers "X"` / `callees "X"` for call-graph questions
```bash
NAV="sh wm-ts-nav/nav"
# Use --root backend for Rust, --root frontend/src for TS/Svelte
$NAV --root backend outline backend/path/to/file.rs # file structure
$NAV --root backend def "ServiceName" # find definition
$NAV --root backend body "decrypt_oauth_data" # extract source code
$NAV --root backend search "%" --parent ServiceName # methods on a type
$NAV --root backend search "Trigger" --kind struct # find by kind
$NAV --root backend refs "X" --file handler.rs --caller # scoped refs with caller
$NAV --root backend callers "X" # who calls X?
$NAV --root backend callees "X" # what does X call?
```
**Limitations** — syntax-level analysis, no type inference. Use **Grep** instead when completeness matters (finding all usages, exhaustiveness checks):
- `refs`/`callers`/`callees` can't follow re-exports, glob imports, or different import paths to the same symbol
- Trait impls, macro-generated symbols (`sqlx::FromRow`), and namespace member access (`ns.X`) are invisible
- `callees` shows all identifiers in a function body, not just actual calls
## Core Principles
- **MUST `outline` before `Read`** on unfamiliar files — then `body` or `Read` with offset/limit for specifics
- Search for existing code to reuse before writing new code
- Follow established patterns in the codebase
- Keep changes focused — don't refactor beyond what's asked

View File

@@ -192,6 +192,70 @@ sandbox:
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.
## Cursor SSH Integration (`wmc`)
`wm-cursor` (aliased as `wmc`) gives each worktree its own Cursor SSH remote window with an independently-focused tmux session. All windows are visible in the status bar across all Cursor terminals, but each one is focused on its own worktree.
This uses **grouped tmux sessions** — multiple sessions that share the same window list but track focus independently:
```
tmux session: main <-- your main Cursor terminal
tmux session: cursor-feat-a <-- Cursor window for feat-a (focused on wm-feat-a)
tmux session: cursor-feat-b <-- Cursor window for feat-b (focused on wm-feat-b)
\__ all three share the same windows in the status bar
```
### Setup
Run once from inside tmux on the remote:
```bash
./scripts/wm-cursor setup /home/hugo/projects/windmill
```
This:
1. **Merges `.vscode/settings.json`** — adds the `wm-tmux` terminal profile (auto-attaches to the `main` tmux session), disables auto port forwarding, configures forwarding for ports 8000/3000/5432, and stops rust-analyzer from auto-starting. Existing settings are preserved.
2. **Creates `.vscode/tasks.json`** — auto-starts the dev database (`start-dev-db.sh`) when the folder opens.
3. **Adds `wmc` alias to `~/.zshrc`** — so you can use `wmc` from any tmux window.
4. **Adds `eval "$(wmc completions)"`** to `~/.zshrc` — provides tab-completion for subcommands and worktree names (for `open`, `open-ee`, and `close`).
After setup, reopen Cursor's terminal to pick up the new profile.
### Usage
All commands run from inside a tmux session (i.e., from Cursor's integrated terminal after setup).
**Create a new worktree + open Cursor:**
```bash
wmc add -A -p "implement feature X"
```
This runs `workmux add`, creates a grouped tmux session, writes `.vscode/settings.json` in the worktree (with port forwarding matching the worktree's assigned ports), and opens a new Cursor window.
**Open Cursor for an existing worktree:**
```bash
wmc open my-feature
```
**Open the EE worktree in Cursor (no tmux session):**
```bash
wmc open-ee my-feature
```
This finds the matching `windmill-ee-private__worktrees/<name>` directory and opens it in a new Cursor window.
**Close a worktree's Cursor window and tmux window (keeps the worktree):**
```bash
wmc close my-feature
```
This kills the grouped tmux session and calls `workmux close` to close the tmux window. The worktree and branch are preserved. Grouped sessions are also automatically cleaned up when you `workmux rm` a worktree (via `scripts/worktree-cleanup`).
## Cargo Features
To build the backend with specific Cargo features (e.g., `enterprise`, `parquet`), pass them via `CARGO_FEATURES`. The backend pane reads this from `.env.local` and appends `--features <value>` to the `cargo watch` command.
@@ -206,6 +270,20 @@ CARGO_FEATURES="enterprise,parquet" wm add my-feature
This gets written to `.env.local` by the `post_create` hook (`scripts/worktree-env`), and the backend pane picks it up automatically.
**With `wmc` (wm-cursor):**
Use the `--features` flag:
```bash
# Create a new worktree with features
wmc add --features "enterprise,parquet" -A -p "implement feature X"
# Open an existing worktree with different features
wmc open my-feature --features "enterprise,parquet"
```
The `--features` flag exports `CARGO_FEATURES` so the `post_create` hook writes it to `.env.local`. When using `wmc open`, it updates the existing `.env.local` with the new features.
## Login
Default credentials: `admin@windmill.dev` / `changeme`

207
backend/Cargo.lock generated
View File

@@ -169,9 +169,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "1.0.0"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -184,15 +184,15 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.14"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
@@ -1860,9 +1860,9 @@ dependencies = [
[[package]]
name = "bon"
version = "3.9.1"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe"
checksum = "2d13a61f2963b88eef9c1be03df65d42f6996dfeac1054870d950fcf66686f83"
dependencies = [
"bon-macros",
"rustversion",
@@ -1870,9 +1870,9 @@ dependencies = [
[[package]]
name = "bon-macros"
version = "3.9.1"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c"
checksum = "d314cc62af2b6b0c65780555abb4d02a03dd3b799cd42419044f0c38d99738c0"
dependencies = [
"darling 0.23.0",
"ident_case",
@@ -2208,9 +2208,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.57"
version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -2323,9 +2323,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.6.0"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2333,9 +2333,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.6.0"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [
"anstream",
"anstyle",
@@ -2345,9 +2345,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.6.0"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -2357,9 +2357,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "1.1.0"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "clipboard-win"
@@ -2418,9 +2418,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.5"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "combine"
@@ -8208,9 +8208,9 @@ dependencies = [
[[package]]
name = "lz4_flex"
version = "0.11.6"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a"
checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
dependencies = [
"twox-hash 2.1.2",
]
@@ -14087,9 +14087,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
@@ -14697,9 +14697,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -15741,7 +15741,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15808,7 +15808,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15821,7 +15821,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"argon2",
@@ -15962,7 +15962,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15985,7 +15985,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15998,7 +15998,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16024,7 +16024,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16034,7 +16034,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16051,7 +16051,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -16074,7 +16074,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16097,7 +16097,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16113,7 +16113,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16133,7 +16133,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16153,7 +16153,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16167,7 +16167,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16195,7 +16195,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16220,7 +16220,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"flate2",
@@ -16238,7 +16238,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16259,7 +16259,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16279,7 +16279,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16309,7 +16309,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16336,7 +16336,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"lazy_static",
"serde",
@@ -16348,7 +16348,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16371,7 +16371,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16385,7 +16385,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16416,7 +16416,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16430,7 +16430,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16449,7 +16449,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16548,7 +16548,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16567,7 +16567,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"regex",
"serde",
@@ -16582,7 +16582,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16606,7 +16606,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"futures",
@@ -16623,7 +16623,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16639,7 +16639,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16660,7 +16660,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16691,7 +16691,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16715,7 +16715,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-stream",
@@ -16749,7 +16749,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"futures",
@@ -16767,7 +16767,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16776,7 +16776,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16788,7 +16788,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16800,7 +16800,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"gosyn",
@@ -16812,7 +16812,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16824,7 +16824,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16836,7 +16836,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -16847,7 +16847,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16858,7 +16858,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16870,7 +16870,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -16881,7 +16881,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16905,7 +16905,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16919,7 +16919,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16936,7 +16936,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16950,7 +16950,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"serde",
@@ -16962,7 +16962,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16980,7 +16980,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -16996,7 +16996,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17012,7 +17012,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"serde",
@@ -17023,7 +17023,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17060,7 +17060,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"const_format",
@@ -17098,7 +17098,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17109,7 +17109,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17138,7 +17138,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -17161,7 +17161,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17194,7 +17194,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17214,7 +17214,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17248,7 +17248,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17283,7 +17283,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17306,7 +17306,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17330,7 +17330,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17354,7 +17354,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17389,7 +17389,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17417,7 +17417,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17440,7 +17440,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17458,7 +17458,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17521,7 +17521,6 @@ dependencies = [
"sha2 0.10.9",
"sqlx",
"tar",
"tempfile",
"tiberius",
"tokio",
"tokio-postgres 0.7.13",
@@ -17565,7 +17564,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.657.2"
version = "1.655.0"
dependencies = [
"bytes",
"futures",

View File

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

View File

@@ -25,50 +25,6 @@ pub mod pydantic_parser;
const FUNCTION_CALL: &str = "<function call>";
/// Get the simple type name from an expression (e.g. `str`, `int`).
fn simple_type_name(e: &Expr) -> Option<&str> {
match e {
Expr::Name(ExprName { id, .. }) => Some(id.as_ref()),
_ => None,
}
}
/// If `e` is `list[T]` or `List[T]`, return the inner expression `T`.
fn list_elem_expr(e: &Expr) -> Option<&Expr> {
match e {
Expr::Subscript(x) => match x.value.as_ref() {
Expr::Name(ExprName { id, .. }) if id == "list" || id == "List" => {
Some(x.slice.as_ref())
}
_ => None,
},
_ => None,
}
}
/// Detect `T | list[T]` or `list[T] | T` union patterns.
/// Returns the original type string (e.g. "str | list[str]") for use as `otyp`.
fn detect_py_union_array_otyp(e: &Expr) -> Option<String> {
let Expr::BinOp(x) = e else { return None };
// T | list[T]
if let (Some(scalar), Some(elem)) = (simple_type_name(&x.left), list_elem_expr(&x.right)) {
if let Some(elem_name) = simple_type_name(elem) {
if scalar == elem_name {
return Some(format!("{} | list[{}]", scalar, elem_name));
}
}
}
// list[T] | T
if let (Some(elem), Some(scalar)) = (list_elem_expr(&x.left), simple_type_name(&x.right)) {
if let Some(elem_name) = simple_type_name(elem) {
if scalar == elem_name {
return Some(format!("list[{}] | {}", elem_name, scalar));
}
}
}
None
}
/// Cheap string-based check to see if code might contain Pydantic models or dataclasses.
/// Returns true if we should do full AST parsing for type detection, false otherwise.
/// This avoids expensive parsing for the common case where scripts don't use these features.
@@ -434,19 +390,8 @@ pub fn parse_python_signature(
_ => {}
}
// Detect T | list[T] union types and set otyp for
// debounce accumulation support. Falls back to docstring
// description if no union array pattern is found.
let union_otyp = params.args[i]
.as_arg()
.annotation
.as_ref()
.and_then(|ann| detect_py_union_array_otyp(ann.as_ref()));
Arg {
otyp: union_otyp.or_else(|| {
metadata.descriptions.get(&arg_name).map(|d| d.to_string())
}),
otyp: metadata.descriptions.get(&arg_name).map(|d| d.to_string()),
name: arg_name,
typ,
has_default: has_default || default.is_some(),
@@ -496,9 +441,6 @@ fn parse_expr(
Expr::Constant(ExprConstant { value: Constant::None, .. })
) {
(parse_expr(&x.left, enums, module).0, true)
} else if detect_py_union_array_otyp(e.as_ref()).is_some() {
// T | list[T] — parsed type is Unknown; otyp is set separately
(Typ::Unknown, false)
} else {
(Typ::Unknown, false)
}
@@ -1104,33 +1046,6 @@ def main(a: str, b: Optional[str], c: str | None): return
Ok(())
}
#[test]
fn test_parse_python_union_array_type() -> anyhow::Result<()> {
let code = r#"
def main(items: str | list[str], numbers: list[int] | int, plain: str):
pass
"#;
let result = parse_python_signature(code, None, false)?;
assert_eq!(result.args.len(), 3);
// str | list[str] → otyp set, typ Unknown
assert_eq!(result.args[0].name, "items");
assert_eq!(result.args[0].otyp, Some("str | list[str]".to_string()));
assert_eq!(result.args[0].typ, Typ::Unknown);
// list[int] | int → otyp set, typ Unknown
assert_eq!(result.args[1].name, "numbers");
assert_eq!(result.args[1].otyp, Some("list[int] | int".to_string()));
assert_eq!(result.args[1].typ, Typ::Unknown);
// plain str → no otyp
assert_eq!(result.args[2].name, "plain");
assert_eq!(result.args[2].otyp, None);
assert_eq!(result.args[2].typ, Typ::Str(None));
Ok(())
}
#[test]
fn test_parse_python_sig_enum() -> anyhow::Result<()> {
let code = r#"

View File

@@ -363,12 +363,8 @@ fn parse_param(
let r = match param.pat {
Pat::Ident(ident) => {
let (name, typ, nullable) = binding_ident_to_arg(symbol_table, type_resolver, &ident);
let otyp = ident
.type_ann
.as_ref()
.and_then(|ta| detect_union_array_otyp(&ta.type_ann));
Ok(Arg {
otyp,
otyp: None,
name,
typ,
default: None,
@@ -378,21 +374,13 @@ fn parse_param(
}
// Pat::Object(ObjectPat { ... }) = todo!()
Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable, otyp) = match *left {
Pat::Ident(ident) => {
let otyp = ident
.type_ann
.as_ref()
.and_then(|ta| detect_union_array_otyp(&ta.type_ann));
let (name, typ, nullable) =
binding_ident_to_arg(symbol_table, type_resolver, &ident);
(name, typ, nullable, otyp)
}
let (name, mut typ, _nullable) = match *left {
Pat::Ident(ident) => binding_ident_to_arg(symbol_table, type_resolver, &ident),
Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
*counter += 1;
let name = format!("anon{}", counter);
(name, typ, nullable, None)
(name, typ, nullable)
}
_ => {
return Err(anyhow::anyhow!(
@@ -428,7 +416,7 @@ fn parse_param(
if typ == Typ::Unknown && dflt.is_some() {
typ = json_to_typ(dflt.as_ref().unwrap(), false);
}
Ok(Arg { otyp, name, typ, default: dflt, has_default: true, oidx: None })
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true, oidx: None })
}
Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
@@ -973,75 +961,6 @@ fn one_of_properties(
.collect()
}
fn ts_type_to_string(ts_type: &TsType) -> Option<String> {
match ts_type {
TsType::TsKeywordType(t) => Some(
match t.kind {
TsKeywordTypeKind::TsStringKeyword => "string",
TsKeywordTypeKind::TsNumberKeyword => "number",
TsKeywordTypeKind::TsBooleanKeyword => "boolean",
TsKeywordTypeKind::TsObjectKeyword => "object",
TsKeywordTypeKind::TsBigIntKeyword => "bigint",
TsKeywordTypeKind::TsAnyKeyword => "any",
_ => return None,
}
.to_string(),
),
TsType::TsTypeRef(TsTypeRef { type_name, .. }) => match type_name {
TsEntityName::Ident(Ident { sym, .. }) => Some(sym.to_string()),
_ => None,
},
_ => None,
}
}
fn get_array_elem_type(ts_type: &TsType) -> Option<&TsType> {
match ts_type {
TsType::TsArrayType(TsArrayType { elem_type, .. }) => Some(elem_type),
_ => None,
}
}
/// Detects union types of the form `T | T[]` or `T[] | T` and returns
/// the original type string (e.g. "string | string[]").
fn detect_union_array_otyp(ts_type: &TsType) -> Option<String> {
let TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType(TsUnionType {
types,
..
})) = ts_type
else {
return None;
};
if types.len() != 2 {
return None;
}
// Check pattern: T | T[]
if let (Some(scalar_name), Some(array_elem)) =
(ts_type_to_string(&types[0]), get_array_elem_type(&types[1]))
{
if let Some(elem_name) = ts_type_to_string(array_elem) {
if scalar_name == elem_name {
return Some(format!("{} | {}[]", scalar_name, elem_name));
}
}
}
// Check pattern: T[] | T
if let (Some(array_elem), Some(scalar_name)) =
(get_array_elem_type(&types[0]), ts_type_to_string(&types[1]))
{
if let Some(elem_name) = ts_type_to_string(array_elem) {
if scalar_name == elem_name {
return Some(format!("{}[] | {}", elem_name, scalar_name));
}
}
}
None
}
fn find_undefined(types: &Vec<Box<TsType>>) -> Option<usize> {
types.into_iter().position(|x| match **x {
TsType::TsKeywordType(TsKeywordType { kind, .. }) => {

View File

@@ -646,55 +646,6 @@ mod tests {
);
}
#[test]
fn test_parse_union_array_type() {
let code = r#"
export async function main(
items: string | string[],
numbers: number[] | number,
plain: string
) {
return { items, numbers, plain };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
name: "items".to_string(),
otyp: Some("string | string[]".to_string()),
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "numbers".to_string(),
otyp: Some("number[] | number".to_string()),
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "plain".to_string(),
otyp: None,
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_invalid_typescript() {
let code = r#"

View File

@@ -221,6 +221,9 @@ pub fn main() -> anyhow::Result<()> {
}
}
// This line intentionally fails compilation to test CI
let _: i32 = this_variable_does_not_exist;
// Normal execution (console/foreground mode)
setup_deno_runtime()?;
create_and_run_current_thread_inner(windmill_main())

View File

@@ -1518,92 +1518,6 @@ Write-Output "hello $msg"
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_powershell_param_block_with_attributes(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let content = r#"
param(
[Parameter(Mandatory=$true)]
[string]$Name,
[int]$Count = 3
)
Write-Output "$Name-$Count"
"#
.to_owned();
let job = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
lock: None,
language: ScriptLang::Powershell,
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(),
}))
.arg("Name", json!("test"))
.arg("Count", json!(7))
.run_until_complete(&db, false, port)
.await;
assert_eq!(job.json_result(), Some(json!("test-7")));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_powershell_error_caught(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Script with param block that throws an error — verifies the catch block works
let content = r#"
param($x)
throw "intentional error"
"#
.to_owned();
let job = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
lock: None,
language: ScriptLang::Powershell,
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(),
}))
.arg("x", json!(1))
.run_until_complete(&db, false, port)
.await;
assert!(!job.success, "job should fail on thrown error");
let result_str = serde_json::to_string(&job.result).unwrap_or_default();
assert!(
result_str.contains("An error occurred:"),
"catch block should output 'An error occurred:', got: {result_str}"
);
assert!(
result_str.contains("intentional error"),
"catch block should output the error message, got: {result_str}"
);
// Verify the catch block doesn't leak "Write-Output" as literal text
// (regression from the old broken line continuation in strict_termination_end)
let after_marker = result_str.split("An error occurred:").nth(1).unwrap_or("");
assert!(
!after_marker.starts_with("\\nWrite-Output"),
"catch block should not output literal 'Write-Output' text, got: {result_str}"
);
Ok(())
}
#[cfg(feature = "php")]
#[sqlx::test(fixtures("base"))]
async fn test_php_job(db: Pool<Postgres>) -> anyhow::Result<()> {

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.657.2
version: 1.655.0
title: Windmill API
contact:

View File

@@ -283,8 +283,6 @@ struct SimplifiedSettings {
#[serde(skip_serializing_if = "Option::is_none")]
operator_settings: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
datatable: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_team_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_name: Option<String>,
@@ -325,8 +323,6 @@ struct SimplifiedSettingsLegacy {
#[serde(skip_serializing_if = "Option::is_none")]
operator_settings: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
datatable: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_team_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_name: Option<String>,
@@ -351,7 +347,6 @@ struct SettingsRow {
mute_critical_alerts: Option<bool>,
color: Option<String>,
operator_settings: Option<serde_json::Value>,
datatable: Option<Value>,
slack_team_id: Option<String>,
slack_name: Option<String>,
slack_command_script: Option<String>,
@@ -960,7 +955,6 @@ pub(crate) async fn tarball_workspace(
mute_critical_alerts,
color,
operator_settings,
datatable,
slack_team_id,
slack_name,
slack_command_script
@@ -989,7 +983,6 @@ pub(crate) async fn tarball_workspace(
mute_critical_alerts: row.mute_critical_alerts,
color: row.color.clone(),
operator_settings: row.operator_settings.clone(),
datatable: row.datatable.clone(),
slack_team_id: row.slack_team_id.clone(),
slack_name: row.slack_name.clone(),
slack_command_script: row.slack_command_script.clone(),
@@ -1052,7 +1045,6 @@ pub(crate) async fn tarball_workspace(
mute_critical_alerts: row.mute_critical_alerts,
color: row.color,
operator_settings: row.operator_settings,
datatable: row.datatable,
slack_team_id: row.slack_team_id,
slack_name: row.slack_name,
slack_command_script: row.slack_command_script,

View File

@@ -3030,16 +3030,8 @@ impl PulledJobResult {
if let Some(s) = str_o.as_ref() {
match serde_json::from_str::<Vec<Box<RawValue>>>(s) {
Ok(ref mut vec) => accumulated_arg.append(vec),
Err(_) => {
// Value is not an array — wrap the scalar into a
// single-element array. This supports union types
// like T | T[] where the caller may pass a bare T.
match RawValue::from_string(s.to_string()) {
Ok(raw) => accumulated_arg.push(raw),
Err(e) => {
return Err(error::Error::ArgumentErr(format!("cannot consolidate argument `{arg_name_to_accumulate}`: value is neither a valid list nor a valid JSON value\nUnwrapped Error: {e}")));
}
}
Err(e) => {
return Err(error::Error::ArgumentErr(format!("cannot consolidate arguments of non-list type. Type provided for argument `{arg_name_to_accumulate}` is not a list\nUnwrapped Error: {e}")));
}
}
}

View File

@@ -143,8 +143,5 @@ hyper-tls = { workspace = true, optional = true }
hyper-util = { workspace = true, optional = true }
rcgen = { workspace = true, optional = true }
[dev-dependencies]
tempfile.workspace = true
[build-dependencies]
libffi-sys = { workspace = true, optional = true }

View File

@@ -18,7 +18,7 @@ const NSJAIL_CONFIG_RUN_POWERSHELL_CONTENT: &str =
include_str!("../nsjail/run.powershell.config.proto");
lazy_static::lazy_static! {
static ref RE_POWERSHELL_IMPORTS: Regex = Regex::new(r#"^\s*Import-Module\s+(?:-Name\s+)?"?([^\s"]+)"?(?:\s+-RequiredVersion\s+"?([^\s"]+)"?)?"#).unwrap();
static ref RE_POWERSHELL_IMPORTS: Regex = Regex::new(r#"^Import-Module\s+(?:-Name\s+)?"?([^\s"]+)"?(?:\s+-RequiredVersion\s+"?([^\s"]+)"?)?"#).unwrap();
}
use crate::{
@@ -196,41 +196,17 @@ async fn get_module_versions(module_path: &str) -> Result<Vec<String>, Error> {
.to_string();
// Check if this looks like a version (contains dots and numbers)
// and verify a module manifest (.psd1) or script (.psm1) actually exists
if version.chars().any(|c| c.is_numeric()) && version.contains('.') {
let has_module_files = fs::read_dir(&version_path)
.map(|entries| {
entries.filter_map(|e| e.ok()).any(|e| {
let name = e.file_name();
let name = name.to_string_lossy();
name.ends_with(".psd1") || name.ends_with(".psm1")
})
})
.unwrap_or(false);
if has_module_files {
versions.push(version);
}
versions.push(version);
}
}
}
}
}
// If no version subdirectories found, check if module files exist directly
// in the module directory (flat/single-version installation)
// If no version subdirectories found, treat as single version installation
if versions.is_empty() {
let has_module_files = fs::read_dir(module_path)
.map(|entries| {
entries.filter_map(|e| e.ok()).any(|e| {
let name = e.file_name();
let name = name.to_string_lossy();
name.ends_with(".psd1") || name.ends_with(".psm1")
})
})
.unwrap_or(false);
if has_module_files {
versions.push("unknown".to_string());
}
versions.push("unknown".to_string());
}
Ok(versions)
@@ -458,26 +434,25 @@ pub async fn handle_powershell_job(
logs2.push_str("\n\n--- POWERSHELL CODE EXECUTION ---\n");
append_logs(&job.id, &job.workspace_id, logs2, db).await;
// Pre-load system modules and cached modules (e.g. WindmillClient), then disable
// autoloading so the large cache dir isn't scanned on every command invocation.
// make sure default (only allhostsallusers) modules are loaded, disable autoload (cache can be large to explore especially on cloud) and add /tmp/windmill/cache to PSModulePath
#[cfg(unix)]
let profile = format!(
"$PSModuleAutoloadingPreference = 'None'
$PSModulePathBackup = $env:PSModulePath
$env:PSModulePath = \"$PSHome/Modules:{}\"
$env:PSModulePath = \"$PSHome/Modules\"
Get-Module -ListAvailable | Import-Module
$env:PSModulePath = \"{}:$PSModulePathBackup\"",
*POWERSHELL_CACHE_DIR, *POWERSHELL_CACHE_DIR
*POWERSHELL_CACHE_DIR
);
#[cfg(windows)]
let profile = format!(
"$PSModuleAutoloadingPreference = 'None'
$PSModulePathBackup = $env:PSModulePath
$env:PSModulePath = \"C:\\Program Files\\PowerShell\\7\\Modules;{}\"
$env:PSModulePath = \"C:\\Program Files\\PowerShell\\7\\Modules\"
Get-Module -ListAvailable | Import-Module
$env:PSModulePath = \"{};$PSModulePathBackup\"",
*POWERSHELL_CACHE_DIR, *POWERSHELL_CACHE_DIR
*POWERSHELL_CACHE_DIR
);
// NOTE: powershell error handling / termination is quite tricky compared to bash
@@ -491,8 +466,8 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
let strict_termination_end = "\n\
} catch {\n\
Write-Output \"An error occurred:\"\n\
Write-Output $_\n\
Write-Output \"An error occurred:\n\"\
Write-Output $_
exit 1\n\
}\n";
@@ -697,231 +672,3 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
"No result.out, result2.out or result.json found"
)))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
// --- RE_POWERSHELL_IMPORTS regex tests ---
fn match_import(line: &str) -> Option<(String, Option<String>)> {
RE_POWERSHELL_IMPORTS.captures(line).map(|cap| {
let name = cap.get(1).unwrap().as_str().to_string();
let version = cap.get(2).map(|m| m.as_str().to_string());
(name, version)
})
}
#[test]
fn test_import_module_basic() {
let (name, version) = match_import("Import-Module WindmillClient").unwrap();
assert_eq!(name, "WindmillClient");
assert_eq!(version, None);
}
#[test]
fn test_import_module_with_leading_whitespace() {
let (name, _) = match_import(" Import-Module WindmillClient").unwrap();
assert_eq!(name, "WindmillClient");
}
#[test]
fn test_import_module_with_tab_indent() {
let (name, _) = match_import("\tImport-Module WindmillClient").unwrap();
assert_eq!(name, "WindmillClient");
}
#[test]
fn test_import_module_with_name_flag() {
let (name, _) = match_import("Import-Module -Name WindmillClient").unwrap();
assert_eq!(name, "WindmillClient");
}
#[test]
fn test_import_module_with_required_version() {
let (name, version) =
match_import(r#"Import-Module WindmillClient -RequiredVersion "1.655.0""#).unwrap();
assert_eq!(name, "WindmillClient");
assert_eq!(version, Some("1.655.0".to_string()));
}
#[test]
fn test_import_module_quoted_name() {
let (name, _) = match_import(r#"Import-Module "WindmillClient""#).unwrap();
assert_eq!(name, "WindmillClient");
}
#[test]
fn test_import_module_name_flag_quoted_with_version() {
let (name, version) =
match_import(r#"Import-Module -Name "WindmillClient" -RequiredVersion "2.0.0""#)
.unwrap();
assert_eq!(name, "WindmillClient");
assert_eq!(version, Some("2.0.0".to_string()));
}
#[test]
fn test_import_module_indented_with_version() {
let (name, version) =
match_import(r#" Import-Module WindmillClient -RequiredVersion 1.0.0"#).unwrap();
assert_eq!(name, "WindmillClient");
assert_eq!(version, Some("1.0.0".to_string()));
}
#[test]
fn test_commented_import_not_matched() {
assert!(match_import("# Import-Module WindmillClient").is_none());
}
// --- get_module_versions / check_module_installed tests ---
#[tokio::test]
async fn test_empty_module_dir_not_installed() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
fs::create_dir(&module_dir).unwrap();
let versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
assert!(versions.is_empty(), "empty dir should have no versions");
}
#[tokio::test]
async fn test_empty_version_subdir_not_installed() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
let version_dir = module_dir.join("1.655.0");
fs::create_dir_all(&version_dir).unwrap();
let versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
assert!(
versions.is_empty(),
"version dir without .psd1/.psm1 should not count"
);
}
#[tokio::test]
async fn test_valid_versioned_module_detected() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
let version_dir = module_dir.join("1.655.0");
fs::create_dir_all(&version_dir).unwrap();
fs::write(version_dir.join("WindmillClient.psd1"), "# manifest").unwrap();
fs::write(version_dir.join("WindmillClient.psm1"), "# module").unwrap();
let versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
assert_eq!(versions, vec!["1.655.0"]);
}
#[tokio::test]
async fn test_flat_module_with_files_detected() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("MyModule");
fs::create_dir(&module_dir).unwrap();
fs::write(module_dir.join("MyModule.psm1"), "# module").unwrap();
let versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
assert_eq!(versions, vec!["unknown"]);
}
#[tokio::test]
async fn test_flat_module_without_files_not_detected() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("MyModule");
fs::create_dir(&module_dir).unwrap();
fs::write(module_dir.join("readme.txt"), "not a module").unwrap();
let versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
assert!(versions.is_empty());
}
#[tokio::test]
async fn test_check_module_installed_empty_dir_returns_false() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
fs::create_dir(&module_dir).unwrap();
let mut dirs = HashMap::new();
dirs.insert(
"windmillclient".to_string(),
module_dir.to_str().unwrap().to_string(),
);
let (installed, _) = check_module_installed(&dirs, "WindmillClient", None)
.await
.unwrap();
assert!(
!installed,
"empty module dir should not be considered installed"
);
}
#[tokio::test]
async fn test_check_module_installed_valid_module_returns_true() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
let version_dir = module_dir.join("1.655.0");
fs::create_dir_all(&version_dir).unwrap();
fs::write(version_dir.join("WindmillClient.psd1"), "# manifest").unwrap();
let mut dirs = HashMap::new();
dirs.insert(
"windmillclient".to_string(),
module_dir.to_str().unwrap().to_string(),
);
let (installed, versions) = check_module_installed(&dirs, "WindmillClient", None)
.await
.unwrap();
assert!(installed);
assert_eq!(versions, vec!["1.655.0"]);
}
#[tokio::test]
async fn test_check_module_installed_wrong_version_returns_false() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
let version_dir = module_dir.join("1.0.0");
fs::create_dir_all(&version_dir).unwrap();
fs::write(version_dir.join("WindmillClient.psd1"), "# manifest").unwrap();
let mut dirs = HashMap::new();
dirs.insert(
"windmillclient".to_string(),
module_dir.to_str().unwrap().to_string(),
);
let (installed, _) = check_module_installed(&dirs, "WindmillClient", Some("2.0.0"))
.await
.unwrap();
assert!(!installed, "wrong version should not match");
}
#[tokio::test]
async fn test_multiple_versions_detected() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
for ver in &["1.0.0", "1.655.0"] {
let version_dir = module_dir.join(ver);
fs::create_dir_all(&version_dir).unwrap();
fs::write(version_dir.join("WindmillClient.psd1"), "# manifest").unwrap();
}
let mut versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
versions.sort();
assert_eq!(versions, vec!["1.0.0", "1.655.0"]);
}
}

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.657.2";
export const VERSION = "v1.655.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -93,16 +93,7 @@ async function generateAppHash(
}
/**
* Result of generating app locks, including which scripts were updated
*/
export interface AppLocksResult {
path: string;
updatedScripts: string[];
}
/**
* Updates locks for inline scripts in an app.
* Returns the path if dry-run, or AppLocksResult with updated scripts if actual update occurred.
* Updates locks for inline scripts in an app
*/
export async function generateAppLocksInternal(
appFolder: string,
@@ -114,7 +105,7 @@ export async function generateAppLocksInternal(
},
justUpdateMetadataLock?: boolean,
noStaleMessage?: boolean
): Promise<string | AppLocksResult | void> {
): Promise<string | void> {
if (appFolder.endsWith(SEP)) {
appFolder = appFolder.substring(0, appFolder.length - 1);
}
@@ -176,8 +167,6 @@ export async function generateAppLocksInternal(
);
}
let updatedScripts: string[] = [];
if (!justUpdateMetadataLock) {
const changedScripts = [];
// Find hashes that do not correspond to previous hashes
@@ -212,14 +201,13 @@ export async function generateAppLocksInternal(
replaceInlineScripts(runnables, runnablesPath + SEP, false);
// Update the app runnables with new locks (writes to separate files)
updatedScripts = await updateRawAppRunnables(
await updateRawAppRunnables(
workspace,
runnables,
remote_path,
appFolder,
filteredDeps,
opts.defaultTs,
noStaleMessage
opts.defaultTs
);
// Note: updateRawAppRunnables now writes each runnable to its own file
} else {
@@ -229,17 +217,14 @@ export async function generateAppLocksInternal(
replaceInlineScripts(normalAppFile.value, appFolder + SEP, false);
// Update the app value with new locks
const result = await updateAppInlineScripts(
normalAppFile.value = await updateAppInlineScripts(
workspace,
normalAppFile.value,
remote_path,
appFolder,
filteredDeps,
opts.defaultTs,
noStaleMessage
opts.defaultTs
);
normalAppFile.value = result.value;
updatedScripts = result.updatedScripts;
// Write the updated app file (only for normal apps, raw apps use separate files)
writeIfChanged(
@@ -266,8 +251,6 @@ export async function generateAppLocksInternal(
if (!noStaleMessage) {
log.info(colors.green(`App ${remote_path} lockfiles updated`));
}
return { path: remote_path, updatedScripts };
}
/**
@@ -357,7 +340,6 @@ async function traverseAndProcessInlineScripts(
* Updates locks for all runnables in a raw app, generating locks inline script by inline script.
* Writes each runnable to its own YAML file in the backend folder (new format).
* Also writes content and lock files to the runnables folder.
* Returns the list of runnable IDs that had their locks updated.
*/
async function updateRawAppRunnables(
workspace: Workspace,
@@ -365,10 +347,8 @@ async function updateRawAppRunnables(
remotePath: string,
appFolder: string,
rawDeps?: Record<string, string>,
defaultTs: "bun" | "deno" = "bun",
noStaleMessage?: boolean
): Promise<string[]> {
const updatedRunnables: string[] = [];
defaultTs: "bun" | "deno" = "bun"
): Promise<void> {
const runnablesFolder = path.join(appFolder, APP_BACKEND_FOLDER);
// Ensure runnables folder exists
@@ -434,11 +414,12 @@ async function updateRawAppRunnables(
continue;
}
if (!noStaleMessage) {
log.info(
colors.gray(`Generating lock for runnable ${runnableId} (${language})`)
);
}
log.info(
colors.gray(
`Generating lock for runnable ${runnableId} (${language})
}`
)
);
try {
const lock = await generateInlineScriptLock(
@@ -478,15 +459,11 @@ async function updateRawAppRunnables(
// Write the runnable to its own YAML file
writeRunnableToBackend(runnablesFolder, runnableId, simplifiedRunnable);
updatedRunnables.push(runnableId);
if (!noStaleMessage) {
log.info(
colors.gray(
` Written ${runnableId}.yaml, ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
}
log.info(
colors.gray(
` Written ${runnableId}.yaml, ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
} catch (error: any) {
log.error(
colors.red(
@@ -497,14 +474,11 @@ async function updateRawAppRunnables(
writeRunnableToBackend(runnablesFolder, runnableId, runnable);
}
}
return updatedRunnables;
}
/**
* Updates locks for all inline scripts in a normal app, similar to updateRawAppRunnables
* but for the app.value structure instead of app.runnables.
* Returns a tuple of [updated app value, list of script names that were updated].
* but for the app.value structure instead of app.runnables
*/
async function updateAppInlineScripts(
workspace: Workspace,
@@ -512,11 +486,9 @@ async function updateAppInlineScripts(
remotePath: string,
appFolder: string,
rawDeps?: Record<string, string>,
defaultTs: "bun" | "deno" = "bun",
noStaleMessage?: boolean
): Promise<{ value: any; updatedScripts: string[] }> {
defaultTs: "bun" | "deno" = "bun"
): Promise<any> {
const pathAssigner = newPathAssigner(defaultTs, { skipInlineScriptSuffix: getNonDottedPaths() });
const updatedScripts: string[] = [];
const processor: InlineScriptProcessor = async (inlineScript, context) => {
const language = inlineScript.language as SupportedLanguage;
@@ -546,15 +518,13 @@ async function updateAppInlineScripts(
try {
let lock: string | undefined;
if (language !== "frontend") {
if (!noStaleMessage) {
log.info(
colors.gray(
`Generating lock for inline script "${scriptName}" at ${context.path.join(
"."
)} (${language})`
)
);
}
log.info(
colors.gray(
`Generating lock for inline script "${scriptName}" at ${context.path.join(
"."
)} (${language})`
)
);
lock = await generateInlineScriptLock(
workspace,
@@ -583,18 +553,11 @@ async function updateAppInlineScripts(
const inlineLockRef =
lock && lock !== "" ? `!inline ${basePath}lock` : "";
if (!noStaleMessage) {
log.info(
colors.gray(
` Written ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
}
// Track that this script was updated (only for non-frontend scripts that needed locks)
if (language !== "frontend") {
updatedScripts.push(scriptName);
}
log.info(
colors.gray(
` Written ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
return {
...inlineScript,
@@ -614,8 +577,7 @@ async function updateAppInlineScripts(
}
};
const updatedValue = await traverseAndProcessInlineScripts(appValue, processor);
return { value: updatedValue, updatedScripts };
return await traverseAndProcessInlineScripts(appValue, processor);
}
/**

View File

@@ -29,10 +29,7 @@ import { FlowFile } from "./flow.ts";
import { FlowValue } from "../../../gen/types.gen.ts";
import { replaceInlineScripts } from "../../../windmill-utils-internal/src/inline-scripts/replacer.ts";
import { workspaceDependenciesLanguages } from "../../utils/script_common.ts";
import {
extractNameFromFolder,
getNonDottedPaths,
} from "../../utils/resource_folders.ts";
import { extractNameFromFolder, getFolderSuffix } from "../../utils/resource_folders.ts";
const TOP_HASH = "__flow_hash";
async function generateFlowHash(
@@ -54,14 +51,6 @@ async function generateFlowHash(
}
return { ...hashes, [TOP_HASH]: await generateHash(JSON.stringify(hashes)) };
}
/**
* Result of generating flow locks, including which scripts were updated
*/
export interface FlowLocksResult {
path: string;
updatedScripts: string[];
}
export async function generateFlowLockInternal(
folder: string,
dryRun: boolean,
@@ -71,7 +60,7 @@ export async function generateFlowLockInternal(
},
justUpdateMetadataLock?: boolean,
noStaleMessage?: boolean
): Promise<string | FlowLocksResult | void> {
): Promise<string | void> {
if (folder.endsWith(SEP)) {
folder = folder.substring(0, folder.length - 1);
}
@@ -120,9 +109,8 @@ export async function generateFlowLockInternal(
}
let changedScripts: string[] = [];
if (!justUpdateMetadataLock) {
const changedScripts = [];
//find hashes that do not correspond to previous hashes
for (const [path, hash] of Object.entries(hashes)) {
if (path == TOP_HASH) {
@@ -160,9 +148,7 @@ export async function generateFlowLockInternal(
filteredDeps
);
const lockAssigner = newPathAssigner(opts.defaultTs ?? "bun", {
skipInlineScriptSuffix: getNonDottedPaths(),
});
const lockAssigner = newPathAssigner(opts.defaultTs ?? "bun");
const inlineScripts = extractInlineScriptsForFlows(
flowValue.value.modules,
{},
@@ -199,13 +185,6 @@ export async function generateFlowLockInternal(
if (!noStaleMessage) {
log.info(colors.green(`Flow ${remote_path} lockfiles updated`));
}
// Return the list of updated scripts (extract just the filename from the path)
const updatedScripts = changedScripts.map(p => {
const parts = p.split(SEP);
return parts[parts.length - 1].replace(/\.[^.]+$/, ""); // Remove extension
});
return { path: remote_path, updatedScripts };
}
/**

View File

@@ -11,15 +11,15 @@ import {
generateScriptMetadataInternal,
getRawWorkspaceDependencies,
} from "../../utils/metadata.ts";
import { generateFlowLockInternal, FlowLocksResult } from "../flow/flow_metadata.ts";
import { generateAppLocksInternal, getAppFolders, AppLocksResult } from "../app/app_metadata.ts";
import { generateFlowLockInternal } from "../flow/flow_metadata.ts";
import { generateAppLocksInternal, getAppFolders } from "../app/app_metadata.ts";
import {
elementsToMap,
FSFSElement,
ignoreF,
} from "../sync/sync.ts";
import { exts } from "../script/script.ts";
import { isFlowPath, isAppPath, isRawAppPath } from "../../utils/resource_folders.ts";
import { isFlowPath, isAppPath } from "../../utils/resource_folders.ts";
import { listSyncCodebases } from "../../utils/codebase.ts";
interface StaleItem {
@@ -82,8 +82,7 @@ async function generateMetadata(
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
ignore(p, isD) ||
isFlowPath(p) ||
isAppPath(p) ||
isRawAppPath(p)
isAppPath(p)
);
},
false,
@@ -193,17 +192,12 @@ async function generateMetadata(
// === Filter by folder if specified ===
let filteredItems = staleItems;
if (folder) {
// Normalize to forward slashes (Windows users may use backslashes)
folder = folder.replaceAll("\\", "/");
// Strip trailing slash to match deprecated flow/app handler behavior
if (folder.endsWith("/")) {
// Strip trailing separator to match deprecated flow/app handler behavior
// (see generateFlowLockInternal line 64-66, generateAppLocksInternal line 109-110)
if (folder.endsWith(SEP)) {
folder = folder.substring(0, folder.length - 1);
}
// Normalize item.folder for comparison (Windows file paths use backslashes)
filteredItems = staleItems.filter((item) => {
const normalizedFolder = item.folder.replaceAll("\\", "/");
return normalizedFolder === folder || normalizedFolder.startsWith(folder + "/");
});
filteredItems = staleItems.filter((item) => item.folder === folder || item.folder.startsWith(folder + SEP));
}
// === Show stale items and confirm ===
@@ -286,23 +280,21 @@ async function generateMetadata(
// Process flows
for (const item of flows) {
current++;
const result = await generateFlowLockInternal(
log.info(`${formatProgress(current)} flow ${colors.cyan(item.path)}`);
await generateFlowLockInternal(
item.folder,
false, // dryRun
workspace,
opts,
false,
true // noStaleMessage - we handle output
) as FlowLocksResult | void;
const scriptsInfo = result?.updatedScripts?.length
? `: ${colors.gray(result.updatedScripts.join(", "))}`
: "";
log.info(`${formatProgress(current)} flow ${colors.cyan(item.path)}${scriptsInfo}`);
);
}
// Process apps
for (const item of apps) {
current++;
const result = await generateAppLocksInternal(
log.info(`${formatProgress(current)} app ${colors.cyan(item.path)}`);
await generateAppLocksInternal(
item.folder,
item.isRawApp!, // rawApp
false, // dryRun
@@ -310,11 +302,7 @@ async function generateMetadata(
opts,
false,
true // noStaleMessage - we handle output
) as AppLocksResult | void;
const scriptsInfo = result?.updatedScripts?.length
? `: ${colors.gray(result.updatedScripts.join(", "))}`
: "";
log.info(`${formatProgress(current)} app ${colors.cyan(item.path)}${scriptsInfo}`);
);
}
log.info("");

View File

@@ -252,16 +252,6 @@ async function initAction(opts: InitOptions) {
}
}
// Read nonDottedPaths from config to specialize generated skills
let nonDottedPaths = true; // default for new inits
try {
const { readConfigFile } = await import("../../core/conf.ts");
const config = await readConfigFile();
nonDottedPaths = config.nonDottedPaths ?? true;
} catch {
// If config can't be read, use default
}
// Create guidance files (AGENTS.md, CLAUDE.md, and Claude skills)
try {
// Generate skills reference section for AGENTS.md
@@ -300,20 +290,6 @@ async function initAction(opts: InitOptions) {
let skillContent = SKILL_CONTENT[skill.name];
if (skillContent) {
// Replace placeholders with actual suffixes based on nonDottedPaths
if (nonDottedPaths) {
skillContent = skillContent
.replaceAll("{{FLOW_SUFFIX}}", "__flow")
.replaceAll("{{APP_SUFFIX}}", "__app")
.replaceAll("{{RAW_APP_SUFFIX}}", "__raw_app")
.replaceAll("{{INLINE_SCRIPT_NAMING}}", "Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).");
} else {
skillContent = skillContent
.replaceAll("{{FLOW_SUFFIX}}", ".flow")
.replaceAll("{{APP_SUFFIX}}", ".app")
.replaceAll("{{RAW_APP_SUFFIX}}", ".raw_app")
.replaceAll("{{INLINE_SCRIPT_NAMING}}", "Inline script files use the `.inline_script.` naming convention (e.g. `a.inline_script.ts`).");
}
// Check if this skill has schemas that need to be appended
const schemaMappings = SCHEMA_MAPPINGS[skill.name];
if (schemaMappings && schemaMappings.length > 0) {

View File

@@ -58,7 +58,6 @@ import {
isFlowInlineScriptPath as isFlowInlineScriptPathInternal,
isFlowPath,
isAppPath,
isRawAppPath,
} from "../../utils/resource_folders.ts";
export interface ScriptFile {
@@ -1028,8 +1027,7 @@ export async function generateMetadata(
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
ignore(p, isD) ||
isFlowPath(p) ||
isAppPath(p) ||
isRawAppPath(p)
isAppPath(p)
);
},
false,

View File

@@ -53,7 +53,6 @@ export interface SimplifiedSettings {
mute_critical_alerts?: boolean;
color?: string;
operator_settings?: any;
datatable?: any;
slack_team_id?: string;
slack_name?: string;
slack_command_script?: string;
@@ -101,7 +100,6 @@ export function migrateToGroupedFormat(settings: any): SimplifiedSettings {
if (settings.mute_critical_alerts !== undefined) result.mute_critical_alerts = settings.mute_critical_alerts;
if (settings.color !== undefined) result.color = settings.color;
if (settings.operator_settings !== undefined) result.operator_settings = settings.operator_settings;
if (settings.datatable !== undefined) result.datatable = settings.datatable;
if (settings.slack_team_id !== undefined) result.slack_team_id = settings.slack_team_id;
if (settings.slack_name !== undefined) result.slack_name = settings.slack_name;
if (settings.slack_command_script !== undefined) result.slack_command_script = settings.slack_command_script;
@@ -194,7 +192,6 @@ export async function pushWorkspaceSettings(
mute_critical_alerts: remoteSettings.mute_critical_alerts,
color: remoteSettings.color,
operator_settings: remoteSettings.operator_settings,
datatable: remoteSettings.datatable,
slack_team_id: remoteSettings.slack_team_id,
slack_name: remoteSettings.slack_name,
slack_command_script: remoteSettings.slack_command_script,
@@ -385,14 +382,6 @@ export async function pushWorkspaceSettings(
});
}
if (!deepEqual(localSettings.datatable, settings.datatable)) {
log.debug(`Updating datatable config...`);
await wmill.editDataTableConfig({
workspace,
requestBody: { settings: localSettings.datatable ?? { datatables: {} } },
});
}
if (localSettings.slack_command_script != settings.slack_command_script) {
log.debug(`Updating slack command script...`);
await wmill.editSlackCommand({

View File

@@ -4236,10 +4236,10 @@ description: MUST use when creating flows.
## CLI Commands
Create a folder ending with \`{{FLOW_SUFFIX}}\` and add a \`flow.yaml\` file with the flow definition.
For rawscript modules, use \`!inline path/to/script.ts\` for the content key. {{INLINE_SCRIPT_NAMING}}
Create a folder ending with \`.flow\` and add a YAML file with the flow definition.
For rawscript modules, use \`!inline path/to/script.ts\` for the content key.
After writing, tell the user they can run:
- \`wmill flow generate-locks <path_to_flow_folder> --yes\` - Generate lock files for the specific flow you modified (e.g. \`wmill flow generate-locks f/my_folder/my_flow{{FLOW_SUFFIX}} --yes\`)
- \`wmill flow generate-locks <path_to_flow_folder> --yes\` - Generate lock files for the specific flow you modified (e.g. \`wmill flow generate-locks f/my_folder/my_flow.flow --yes\`)
- \`wmill sync push\` - Deploy to Windmill
Do NOT run these commands yourself. Instead, inform the user that they should run them.
@@ -4375,7 +4375,7 @@ This interactive command creates a complete app structure with your choice of fr
## App Structure
\`\`\`
my_app{{RAW_APP_SUFFIX}}/
my_app.raw_app/
├── AGENTS.md # AI agent instructions (auto-generated)
├── DATATABLES.md # Database schemas (run 'wmill app generate-agents' to refresh)
├── raw_app.yaml # App configuration (summary, path, data settings)
@@ -5072,23 +5072,6 @@ folder related commands
- \`folder add-missing\` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
- \`-y, --yes\` - skip confirmation prompt
### generate-metadata
Generate metadata (locks, schemas) for all scripts, flows, and apps
**Arguments:** \`[folder:string]\`
**Options:**
- \`--yes\` - Skip confirmation prompt
- \`--dry-run\` - Show what would be updated without making changes
- \`--lock-only\` - Re-generate only the lock files
- \`--schema-only\` - Re-generate only script schemas (skips flows and apps)
- \`--skip-scripts\` - Skip processing scripts
- \`--skip-flows\` - Skip processing flows
- \`--skip-apps\` - Skip processing apps
- \`-i --includes <patterns:file[]>\` - Comma separated patterns to specify which files to include
- \`-e --excludes <patterns:file[]>\` - Comma separated patterns to specify which files to exclude
### gitsync-settings
Manage git-sync settings between local wmill.yaml and Windmill backend

View File

@@ -68,7 +68,7 @@ export {
workspaceAdd,
};
export const VERSION = "1.657.2";
export const VERSION = "1.655.0";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";

View File

@@ -50,25 +50,26 @@ export class LockfileGenerationError extends Error {
}
}
export async function generateAllMetadata() {}
export async function getRawWorkspaceDependencies(): Promise<Record<string, string>> {
const rawWorkspaceDeps: Record<string, string> = {};
try {
const entries = await readdir("dependencies", { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) continue;
const filePath = `dependencies/${entry.name}`;
const content = await readFile(filePath, "utf-8");
// Find matching language
for (const lang of workspaceDependenciesLanguages) {
if (entry.name.endsWith(lang.filename)) {
// Check if out of sync
const contentHash = await generateHash(content + filePath);
const isUpToDate = await checkifMetadataUptodate(filePath, contentHash, undefined);
if (!isUpToDate) {
rawWorkspaceDeps[filePath] = content;
}
@@ -690,11 +691,6 @@ export async function inferSchema(
argSigToJsonSchemaType(arg.typ, currentSchema.properties[arg.name]);
// For T | T[] detection for debouncing arg accumulation
if ((arg as any).otyp && (arg as any).otyp.includes('[') && (arg as any).otyp.includes('|')) {
currentSchema.properties[arg.name].originalType = (arg as any).otyp
}
currentSchema.properties[arg.name].default = arg.default;
if (!arg.has_default && !currentSchema.required.includes(arg.name)) {

View File

@@ -1,226 +0,0 @@
/**
* Datatable settings sync tests
*
* Tests that datatable config is correctly synced via settings.yaml during pull/push operations.
*/
import { expect, test } from "bun:test";
import { writeFile, readFile } from "node:fs/promises";
import { parse, stringify } from "yaml";
import { withTestBackend } from "./test_backend.ts";
import { shouldSkipOnCI } from "./cargo_backend.ts";
import { addWorkspace } from "../workspace.ts";
test.skipIf(shouldSkipOnCI())("Datatable config: included in sync pull settings.yaml", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "datatable_pull_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Configure datatable on backend
const datatableConfig = {
datatables: {
main: {
database: {
resource_path: "u/test/test_db",
resource_type: "postgresql"
}
}
}
};
const configResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/workspaces/edit_datatable_config`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ settings: datatableConfig })
}
);
expect(configResp.ok).toBe(true);
// Create wmill.yaml with includeSettings
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
includeSettings: true`, "utf-8");
// Pull settings
const result = await backend.runCLICommand(['sync', 'pull', '--yes'], tempDir);
expect(result.code).toEqual(0);
// Verify settings.yaml was created and contains datatable
const settingsContent = await readFile(`${tempDir}/settings.yaml`, "utf-8");
expect(settingsContent).toContain("datatable:");
expect(settingsContent).toContain("u/test/test_db");
expect(settingsContent).toContain("postgresql");
});
});
test.skipIf(shouldSkipOnCI())("Datatable config: pushed correctly from settings.yaml", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "datatable_push_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Create wmill.yaml with includeSettings
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
includeSettings: true`, "utf-8");
// First pull to get baseline settings
const pullResult = await backend.runCLICommand(['sync', 'pull', '--yes'], tempDir);
expect(pullResult.code).toEqual(0);
// Read existing settings and add datatable config
const settingsContent = await readFile(`${tempDir}/settings.yaml`, "utf-8");
const existingSettings = parse(settingsContent) as Record<string, unknown>;
existingSettings.datatable = {
datatables: {
analytics: {
database: {
resource_path: "u/admin/analytics_db",
resource_type: "postgresql"
}
}
}
};
await writeFile(`${tempDir}/settings.yaml`, stringify(existingSettings), "utf-8");
// Push the modified settings
const pushResult = await backend.runCLICommand(['sync', 'push', '--yes'], tempDir);
expect(pushResult.code).toEqual(0);
// Verify the backend has the updated datatable config
const settingsResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/workspaces/get_settings`
);
expect(settingsResp.ok).toBe(true);
const settings = await settingsResp.json();
expect(settings.datatable).toBeDefined();
expect(settings.datatable.datatables).toBeDefined();
expect(settings.datatable.datatables.analytics).toBeDefined();
expect(settings.datatable.datatables.analytics.database.resource_path).toEqual("u/admin/analytics_db");
expect(settings.datatable.datatables.analytics.database.resource_type).toEqual("postgresql");
});
});
test.skipIf(shouldSkipOnCI())("Datatable config: empty/undefined doesn't cause errors", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "datatable_empty_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Ensure datatable config is empty/cleared on backend
const clearResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/workspaces/edit_datatable_config`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ settings: { datatables: {} } })
}
);
expect(clearResp.ok).toBe(true);
// Create wmill.yaml with includeSettings
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
includeSettings: true`, "utf-8");
// Pull should succeed even with empty datatable config
const pullResult = await backend.runCLICommand(['sync', 'pull', '--yes'], tempDir);
expect(pullResult.code).toEqual(0);
// Settings.yaml should exist
const settingsContent = await readFile(`${tempDir}/settings.yaml`, "utf-8");
expect(settingsContent.length).toBeGreaterThan(0);
// Push should also succeed
const pushResult = await backend.runCLICommand(['sync', 'push', '--yes'], tempDir);
expect(pushResult.code).toEqual(0);
});
});
test.skipIf(shouldSkipOnCI())("Datatable config: round-trip preserves structure", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "datatable_roundtrip_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Set up a complex datatable config on backend with multiple datatables
const originalConfig = {
datatables: {
users: {
database: {
resource_path: "f/shared/users_db",
resource_type: "postgresql"
}
},
logs: {
database: {
resource_path: "f/shared/logs_db",
resource_type: "instance"
}
}
}
};
const configResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/workspaces/edit_datatable_config`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ settings: originalConfig })
}
);
expect(configResp.ok).toBe(true);
// Create wmill.yaml with includeSettings
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
includeSettings: true`, "utf-8");
// Pull
const pullResult = await backend.runCLICommand(['sync', 'pull', '--yes'], tempDir);
expect(pullResult.code).toEqual(0);
// Push back without modification
const pushResult = await backend.runCLICommand(['sync', 'push', '--yes'], tempDir);
expect(pushResult.code).toEqual(0);
// Verify the config is preserved
const settingsResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/workspaces/get_settings`
);
expect(settingsResp.ok).toBe(true);
const settings = await settingsResp.json();
expect(settings.datatable).toBeDefined();
expect(settings.datatable.datatables.users).toBeDefined();
expect(settings.datatable.datatables.logs).toBeDefined();
expect(settings.datatable.datatables.users.database.resource_path).toEqual("f/shared/users_db");
expect(settings.datatable.datatables.logs.database.resource_type).toEqual("instance");
});
});

View File

@@ -107,7 +107,7 @@ async function readFileContent(filePath: string): Promise<string> {
* Create a raw app directory structure on disk
* Uses .raw_app folder suffix with raw_app.yaml metadata
*/
async function createRawAppOnDisk(appDir: string, includeBackend: boolean = false): Promise<void> {
async function createRawAppOnDisk(appDir: string): Promise<void> {
await mkdir(appDir, { recursive: true });
await mkdir(path.join(appDir, "inline_scripts"), { recursive: true });
@@ -131,16 +131,6 @@ async function createRawAppOnDisk(appDir: string, includeBackend: boolean = fals
INLINE_SCRIPT_A_LOCK,
"utf-8"
);
// Optionally create backend runnable (type: inline)
if (includeBackend) {
await mkdir(path.join(appDir, "backend"), { recursive: true });
await writeFile(path.join(appDir, "backend", "query.yaml"), "type: inline\n", "utf-8");
await writeFile(path.join(appDir, "backend", "query.ts"), `export async function main(x: number): Promise<string> {
return \`Result: \${x}\`;
}
`, "utf-8");
}
}
test("Raw App: full sync workflow - push, pull, modify, push, clear, pull", async () => {
@@ -163,7 +153,7 @@ excludes: []`, "utf-8");
// Create folder structure
const appDir = path.join(tempDir, "f", "test", "my_raw_app.raw_app");
await mkdir(path.join(tempDir, "f", "test"), { recursive: true });
await createRawAppOnDisk(appDir, true); // Include backend for metadata test
await createRawAppOnDisk(appDir);
// =========================================================================
// STEP 1: Initial push - create raw app on backend
@@ -276,47 +266,6 @@ excludes: []`, "utf-8");
const pulledInlineScript = await readFileContent(inlineScriptPath);
expect(pulledInlineScript).toContain("modified:");
// =========================================================================
// STEP 7: Test that script generate-metadata does NOT process backend runnables
// =========================================================================
// Create a standalone script (should be processed by script generate-metadata)
await writeFile(path.join(tempDir, "f", "test", "standalone.ts"), `export async function main(): Promise<string> {
return "hello";
}
`, "utf-8");
// Run script generate-metadata
const metaResult1 = await backend.runCLICommand(
['script', 'generate-metadata', '--yes'],
tempDir, "raw_app_test"
);
expect(metaResult1.code).toEqual(0);
// Run generate-metadata --skip-flows --skip-apps
const metaResult2 = await backend.runCLICommand(
['generate-metadata', '--skip-flows', '--skip-apps', '--yes'],
tempDir, "raw_app_test"
);
expect(metaResult2.code).toEqual(0);
// Backend runnables should NOT have .script.yaml files
const backendDir = path.join(appDir, "backend");
expect(await fileExists(path.join(backendDir, "query.yaml"))).toBeTruthy();
expect(await fileExists(path.join(backendDir, "query.ts"))).toBeTruthy();
expect(await fileExists(path.join(backendDir, "query.script.yaml"))).toBeFalsy();
expect(await fileExists(path.join(backendDir, "query.script.lock"))).toBeFalsy();
// Bug: raw app backend files get misprocessed and create script files at wrong location
// The path f/test/my_raw_app.raw_app/backend/query.ts gets truncated at first "."
// becoming f/test/my_raw_app.script.yaml (stripping .raw_app/backend/query.ts)
expect(await fileExists(path.join(tempDir, "f", "test", "my_raw_app.script.yaml"))).toBeFalsy();
expect(await fileExists(path.join(tempDir, "f", "test", "my_raw_app.script.lock"))).toBeFalsy();
// Standalone script SHOULD have metadata
expect(await fileExists(path.join(tempDir, "f", "test", "standalone.script.yaml"))).toBeTruthy();
expect(await fileExists(path.join(tempDir, "f", "test", "standalone.script.lock"))).toBeTruthy();
});
});

View File

@@ -1768,66 +1768,6 @@ excludes: []
});
});
test("Integration: Sync pull with nonDottedPaths uses non-dotted inline script filenames", async () => {
await withTestBackend(async (backend, tempDir) => {
// Push a flow using default dotted paths
await writeFile(
`${tempDir}/wmill.yaml`,
`defaultTs: bun
includes:
- "**"
excludes: []
`,
"utf-8",
);
const uniqueId = Date.now();
const flowName = `f/test/nondot_pull_inline_${uniqueId}`;
const flowFixture = createFlowFixture(flowName);
await mkdir(`${tempDir}/f/test/nondot_pull_inline_${uniqueId}${getFolderSuffix("flow")}`, { recursive: true });
for (const file of Object.values(flowFixture)) {
await writeFile(`${tempDir}/${file.path}`, file.content, "utf-8");
}
const pushResult = await backend.runCLICommand(
["sync", "push", "--yes", "--includes", `f/test/nondot_pull_inline_${uniqueId}*/**`],
tempDir,
);
expect(pushResult.code).toEqual(0);
// Pull into a fresh directory with nonDottedPaths enabled
const tempDir2 = await mkdtemp(join(tmpdir(), "wmill_nondot_inline_"));
try {
await writeFile(
`${tempDir2}/wmill.yaml`,
`defaultTs: bun
nonDottedPaths: true
includes:
- "**"
excludes: []
`,
"utf-8",
);
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir2);
expect(pullResult.code).toEqual(0);
// Verify pulled files use non-dotted inline script naming
const files = await listFilesRecursive(tempDir2);
const flowFiles = files.filter((f) => f.includes(`nondot_pull_inline_${uniqueId}`));
expect(flowFiles.length > 0).toBeTruthy();
// Should use __flow folder, not .flow
expect(flowFiles.some((f) => f.includes("__flow/"))).toBeTruthy();
// No files should have .inline_script. in their name
const dottedInlineFiles = flowFiles.filter((f) => f.includes(".inline_script."));
expect(dottedInlineFiles.length).toEqual(0);
} finally {
await cleanupTempDir(tempDir2);
}
});
});
// =============================================================================
// ws_error_handler_muted Persistence Tests
// =============================================================================

View File

@@ -8,7 +8,7 @@
import { expect, test, describe } from "bun:test";
import { withTestBackend } from "./test_backend.ts";
import { addWorkspace } from "../workspace.ts";
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { writeFile } from "node:fs/promises";
import {
createLocalScript,
createLocalFlow,
@@ -19,12 +19,7 @@ import {
/**
* Helper to set up a workspace with wmill.yaml
*/
async function setupWorkspace(
backend: any,
tempDir: string,
workspaceName: string,
nonDottedPaths = false
) {
async function setupWorkspace(backend: any, tempDir: string, workspaceName: string) {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
@@ -34,88 +29,11 @@ async function setupWorkspace(
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
${nonDottedPaths ? "nonDottedPaths: true\n" : ""}includes:
includes:
- "**"
excludes: []`, "utf-8");
}
async function createLocalNonDottedFlow(tempDir: string, name: string) {
const flowDir = `${tempDir}/f/test/${name}__flow`;
await mkdir(flowDir, { recursive: true });
await writeFile(
`${flowDir}/a.ts`,
`export async function main() {\n return "Hello from flow ${name}";\n}`,
"utf-8"
);
await writeFile(
`${flowDir}/flow.yaml`,
`summary: "${name} flow"
description: "A flow for testing"
value:
modules:
- id: a
value:
type: rawscript
content: "!inline a.ts"
language: bun
input_transforms: {}
schema:
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
properties: {}
required: []
`,
"utf-8"
);
}
async function createLocalNonDottedApp(tempDir: string, name: string) {
const appDir = `${tempDir}/f/test/${name}__app`;
await mkdir(appDir, { recursive: true });
await writeFile(
`${appDir}/app.yaml`,
`summary: "${name} app"
value:
type: app
grid:
- id: button1
data:
type: buttoncomponent
componentInput:
type: runnable
runnable:
type: runnableByName
inlineScript:
content: |
export async function main() {
return "hello from app";
}
language: bun
hiddenInlineScripts: []
css: {}
norefreshbar: false
policy:
on_behalf_of: null
on_behalf_of_email: null
triggerables: {}
execution_mode: viewer
`,
"utf-8"
);
}
async function fileExists(filePath: string): Promise<boolean> {
try {
await stat(filePath);
return true;
} catch {
return false;
}
}
// =============================================================================
// Main test: processes scripts, flows, and apps together
// =============================================================================
@@ -238,87 +156,6 @@ describe("generate-metadata flags", () => {
});
});
test("--lock-only preserves non-dotted flow filenames", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspace(backend, tempDir, "lock_only_non_dotted_test", true);
await createLocalNonDottedFlow(tempDir, "my_flow");
const result = await backend.runCLICommand(
["generate-metadata", "--yes", "--lock-only"],
tempDir,
"lock_only_non_dotted_test"
);
expect(result.code).toEqual(0);
const flowDir = `${tempDir}/f/test/my_flow__flow`;
const flowYaml = await readFile(`${flowDir}/flow.yaml`, "utf-8");
expect(flowYaml).toContain("!inline a.ts");
expect(flowYaml).toContain("!inline a.lock");
expect(flowYaml).not.toContain(".inline_script.");
expect(await fileExists(`${flowDir}/a.lock`)).toEqual(true);
expect(await fileExists(`${flowDir}/a.inline_script.ts`)).toEqual(false);
expect(await fileExists(`${flowDir}/a.inline_script.lock`)).toEqual(false);
});
});
test("generate-metadata preserves non-dotted flow inline script filenames", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspace(backend, tempDir, "full_gen_non_dotted_flow_test", true);
await createLocalNonDottedFlow(tempDir, "my_flow");
const result = await backend.runCLICommand(
["generate-metadata", "--yes"],
tempDir,
"full_gen_non_dotted_flow_test"
);
expect(result.code).toEqual(0);
const flowDir = `${tempDir}/f/test/my_flow__flow`;
const flowYaml = await readFile(`${flowDir}/flow.yaml`, "utf-8");
// Inline script references should use non-dotted naming
expect(flowYaml).toContain("!inline a.ts");
expect(flowYaml).toContain("!inline a.lock");
expect(flowYaml).not.toContain(".inline_script.");
expect(await fileExists(`${flowDir}/a.ts`)).toEqual(true);
expect(await fileExists(`${flowDir}/a.lock`)).toEqual(true);
expect(await fileExists(`${flowDir}/a.inline_script.ts`)).toEqual(false);
expect(await fileExists(`${flowDir}/a.inline_script.lock`)).toEqual(false);
});
});
test("generate-metadata uses non-dotted app inline script filenames", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspace(backend, tempDir, "non_dotted_app_gen_test", true);
await createLocalNonDottedApp(tempDir, "my_app");
const result = await backend.runCLICommand(
["generate-metadata", "--yes"],
tempDir,
"non_dotted_app_gen_test"
);
expect(result.code).toEqual(0);
const appDir = `${tempDir}/f/test/my_app__app`;
const appYaml = await readFile(`${appDir}/app.yaml`, "utf-8");
// Inline script references should use non-dotted naming
expect(appYaml).not.toContain(".inline_script.");
// Verify no dotted inline script files were created
const { readdir: readdirAsync } = await import("node:fs/promises");
const files = await readdirAsync(appDir);
const dottedFiles = files.filter((f: string) => f.includes(".inline_script."));
expect(dottedFiles.length).toEqual(0);
});
});
test("--schema-only only processes scripts (skips flows and apps)", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspace(backend, tempDir, "schema_only_test");

View File

@@ -15,22 +15,17 @@
- Standard location: `~/windmill-ee-private`
- Worktree location: `~/windmill-ee-private__worktrees/<branch-name>/`
## Detecting EE Changes
The `*_ee.rs` files in the windmill repo are symlinks — changes won't appear in `git diff` of the windmill repo. Check the EE repo directly: `git -C <ee-path> status --short`
## EE PR Workflow (MUST DO when modifying `*_ee.rs` files)
When you modify any `*_ee.rs` file and create a PR on windmill:
1. **Prefix the windmill PR title** with `[ee]`: `[ee] <type>: <description>`
2. **Create a matching branch** in `windmill-ee-private` (same branch name)
3. **Commit and push** the `_ee.rs` changes in that branch
4. **Create a companion PR** on `windmill-ee-private` with a link to the windmill PR (no `[ee]` prefix on this one)
5. **Update `ee-repo-ref.txt`**: Run `bash write_latest_ee_ref.sh` from `backend/`
1. **Create a matching branch** in `windmill-ee-private` (same 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/`
- **Verify** it wrote the correct commit hash from your branch, not from main (the script may fall back to `~/windmill-ee-private` on main)
- If wrong, manually write the correct hash
6. **Commit `ee-repo-ref.txt`** in the windmill repo so CI picks up the correct EE ref
5. **Commit `ee-repo-ref.txt`** in the windmill repo so CI picks up the correct EE ref
## Validation

View File

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.657.2",
"version": "1.655.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.657.2",
"version": "1.655.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -81,11 +81,11 @@
"windmill-parser-wasm-java": "1.510.1",
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.647.1",
"windmill-parser-wasm-py": "1.655.0",
"windmill-parser-wasm-py": "1.653.0",
"windmill-parser-wasm-regex": "1.653.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.647.1",
"windmill-parser-wasm-ts": "1.655.0",
"windmill-parser-wasm-ts": "1.653.0",
"windmill-parser-wasm-yaml": "1.593.0",
"windmill-sql-datatype-parser-wasm": "1.512.0",
"windmill-utils-internal": "^1.3.4",
@@ -837,7 +837,6 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -849,7 +848,6 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -860,7 +858,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1350,7 +1347,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1507,7 +1503,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1524,7 +1519,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1541,7 +1535,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1558,7 +1551,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1575,7 +1567,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1592,7 +1583,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1609,7 +1599,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1626,7 +1615,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1643,7 +1631,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1660,7 +1647,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1677,7 +1663,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1694,7 +1679,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1711,7 +1695,6 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1728,7 +1711,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1745,7 +1727,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2051,7 +2032,6 @@
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -6860,7 +6840,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -7359,7 +7339,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7380,7 +7359,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7401,7 +7379,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7422,7 +7399,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7443,7 +7419,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7464,7 +7439,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7485,7 +7459,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7506,7 +7479,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7527,7 +7499,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7548,7 +7519,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7569,7 +7539,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -12145,21 +12114,6 @@
}
}
},
"node_modules/svelte-check/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/svelte-eslint-parser": {
"version": "0.43.0",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
@@ -12890,7 +12844,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -13671,9 +13625,9 @@
"integrity": "sha512-u2qaMkupSdhJibxvkLh3r/y36IARvnYNTLXWvOKxcQ0G/BPUB4+yF5o/yf47vv9zUV5WZv4mrdsKDt/pZDYeDg=="
},
"node_modules/windmill-parser-wasm-py": {
"version": "1.655.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.655.0.tgz",
"integrity": "sha512-kWpAE2Me/8KwNqq4n+0ZoEqMJZ9aCZf1744j0S6Om+F+XyHFR3e7GYK/slJ4cF7zA9TF6mKUSiW4oHJ+WUlNXQ=="
"version": "1.653.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.653.0.tgz",
"integrity": "sha512-vMkSL3JpELpag7nmyGA8onhYNiAG3K1mkh2k4vwVHC3W5dUd12fSS9gsBco2FqPGUPnWv1gCaHwmjBrOBVGL1w=="
},
"node_modules/windmill-parser-wasm-regex": {
"version": "1.653.0",
@@ -13691,9 +13645,9 @@
"integrity": "sha512-9yGLYZX2Hn9TdTqGY/5Fp50ftzgUsrfBkSK9vJkKJd5Amyg+yXLBGzd8pz6Org+4uxMenz/16wpsgijvo6uhhQ=="
},
"node_modules/windmill-parser-wasm-ts": {
"version": "1.655.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.655.0.tgz",
"integrity": "sha512-nzvxILV68MccOKuZcGwjZLB+mWzPMMqEP0FTOb9nmkRWzKPFMxs5ADAXrDZINeNLNv64hYQhGw66ouq0P1G6jQ=="
"version": "1.653.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.653.0.tgz",
"integrity": "sha512-zwBUy7ijo58ooAKcsYflISY/+xllCw3Aq34Kj1PED6uABWVbV6A8MWHqEsjiVzC6iWfihWNtZJpck8zsRr9DCg=="
},
"node_modules/windmill-parser-wasm-yaml": {
"version": "1.593.0",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.657.2",
"version": "1.655.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -154,11 +154,11 @@
"windmill-parser-wasm-java": "1.510.1",
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.647.1",
"windmill-parser-wasm-py": "1.655.0",
"windmill-parser-wasm-py": "1.653.0",
"windmill-parser-wasm-regex": "1.653.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.647.1",
"windmill-parser-wasm-ts": "1.655.0",
"windmill-parser-wasm-ts": "1.653.0",
"windmill-parser-wasm-yaml": "1.593.0",
"windmill-sql-datatype-parser-wasm": "1.512.0",
"windmill-utils-internal": "^1.3.4",
@@ -583,4 +583,4 @@
"@rollup/rollup-linux-x64-gnu": "^4.35.0",
"fsevents": "^2.3.3"
}
}
}

View File

@@ -291,12 +291,7 @@
workers.some(([_, pings]) => pings.some((p) => p.native_mode === true)))
)
let nonNativeTags = $derived(
(nconfig?.worker_tags ?? []).filter(
(t) =>
!nativeTags.some((nt) => t === nt || t.startsWith(`${nt}-`)) &&
t !== 'flow' &&
!t.startsWith('flow-')
)
(nconfig?.worker_tags ?? []).filter((t) => !nativeTags.includes(t) && t !== 'flow')
)
let isAutoNativeMode = $derived(name === 'native')
let isNativeModeEnabled = $derived(nconfig?.native_mode === true || isAutoNativeMode)
@@ -574,8 +569,8 @@
<Alert size="xs" type="warning" title="Non-native tags detected">
This worker group has native mode enabled but includes non-native tags: {nonNativeTags.join(
', '
)}. This is fine if jobs sent to those tags are native only, otherwise they will be
failed.
)}. Non-native jobs will be failed. This is fine if those custom tags are only used
for native language jobs.
</Alert>
{/if}
{#if isNativeModeEnabled && nconfig?.worker_tags != undefined && !nconfig.worker_tags.includes(defaultTagPerWorkspace && workspaceTag ? `flow-${workspaceTag}` : 'flow')}

View File

@@ -1,16 +1,5 @@
<script lang="ts" module>
export let openedDrawers: { val: string[] } = $state({ val: [] })
// When a disposable with minZIndex is open, all disposables use that as
// their z-index base so that overlays opened on top (e.g. a Drawer from
// inside a Modal) stack correctly above it.
// We track per-id entries so concurrent modals don't clobber each other
// (closing one must not reset the base while another is still open).
let minZIndexEntries: Record<string, number> = $state({})
let activeMinZIndex = $derived.by(() => {
const values = Object.values(minZIndexEntries)
return values.length > 0 ? Math.max(...values) : 0
})
</script>
<script lang="ts">
@@ -22,11 +11,6 @@
id?: any
preventEscape?: boolean
initialOffset?: number
/** Minimum z-index base for this overlay. While any disposable with a
* minZIndex is open, all disposables use that as their base so that
* subsequent overlays stack above it (e.g. zIndexes.aiChat + 1 for
* modals that need to render above the AI chat panel). */
minZIndex?: number
children?: import('svelte').Snippet<[any]>
onOpen?: () => void
onClose?: () => void
@@ -37,17 +21,13 @@
id = (Math.random() + 1).toString(36).substring(10),
preventEscape = false,
initialOffset = 0,
minZIndex = 0,
children,
onOpen,
onClose
}: Props = $props()
let offset = $state(untrack(() => initialOffset))
// Note: when a Modal with minZIndex is open, all disposables (including
// already-open Drawers) are elevated. This is acceptable — relative
// stacking order is preserved by the per-instance offset.
let zIndex = $derived(Math.max(zIndexes.disposables, activeMinZIndex) + offset)
let zIndex = $derived(zIndexes.disposables + offset)
export function toggleDrawer() {
if (!open) {
@@ -64,9 +44,6 @@
}
openedDrawers.val.push(id)
offset = initialOffset + openedDrawers.val.length
if (minZIndex > 0) {
minZIndexEntries[id] = minZIndex
}
}
export function closeDrawer() {
@@ -74,9 +51,6 @@
offset = initialOffset
if (openedDrawers.val.includes(id)) {
openedDrawers.val = openedDrawers.val.filter((drawer) => drawer !== id)
if (minZIndex > 0) {
delete minZIndexEntries[id]
}
}
}
@@ -115,9 +89,6 @@
if (open) {
openedDrawers.val.push(untrack(() => id))
offset = untrack(() => initialOffset) + openedDrawers.val.length
if (minZIndex > 0) {
minZIndexEntries[untrack(() => id)] = minZIndex
}
}
let wasEverOpen = false

View File

@@ -2,14 +2,11 @@
import { createBubbler, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
import { createEventDispatcher, untrack } from 'svelte'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
import Button from '../button/Button.svelte'
import { twMerge } from 'tailwind-merge'
import CloseButton from '../CloseButton.svelte'
import Disposable from '../drawer/Disposable.svelte'
import { zIndexes } from '$lib/zIndexes'
import { chatState } from '$lib/components/copilot/chat/sharedChatState.svelte'
interface Props {
title: string
@@ -31,28 +28,12 @@
cancelText = undefined,
kind = 'button',
settings,
children: children_render,
children,
actions
}: Props = $props()
const dispatch = createEventDispatcher()
let disposable: Disposable | undefined = $state(undefined)
// Only elevate above the AI chat panel when it's actually open —
// when chat is closed there's nothing at z-index 1200 to stack above.
const minZIndex = $derived(chatState.size > 0 ? zIndexes.aiChat + 1 : 0)
// Both `bind:open` and this $effect are needed: bind:open syncs the
// boolean, while the effect calls openDrawer/closeDrawer to register
// the disposable in the stacking system (same pattern as Drawer.svelte).
$effect(() => {
open
untrack(() => {
open ? disposable?.openDrawer() : disposable?.closeDrawer()
})
})
function onKeyDown(event: KeyboardEvent) {
if (open) {
switch (event.key) {
@@ -77,80 +58,70 @@
<svelte:window onkeydowncapture={onKeyDown} />
<Disposable bind:open bind:this={disposable} preventEscape {minZIndex}>
{#snippet children({ zIndex })}
{#if open}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
onclick={() => (open = false)}
transition:fadeFast|local
class="fixed top-0 bottom-0 left-0 right-0"
style="z-index: {zIndex}"
role="dialog"
tabindex="-1"
>
{#if open}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
onclick={() => (open = false)}
transition:fadeFast|local
class={'fixed top-0 bottom-0 left-0 right-0 z-[9999]'}
role="dialog"
tabindex="-1"
>
<div
class={twMerge(
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
open ? 'ease-out duration-300 opacity-100' : 'ease-in duration-200 opacity-0'
)}
></div>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
onclick={stopPropagation(bubble('click'))}
class={twMerge(
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
'relative transform overflow-hidden rounded-md bg-surface px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6',
c,
open
? 'ease-out duration-300 opacity-100'
: 'ease-in duration-200 opacity-0'
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
: 'ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
)}
></div>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
onclick={stopPropagation(bubble('click'))}
class={twMerge(
'relative transform overflow-hidden rounded-md bg-surface px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6',
c,
open
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
: 'ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
)}
{style}
{style}
>
{#if kind == 'X'}
<div class="absolute top-4 right-4"><CloseButton on:close={() => (open = false)} /></div
>
{#if kind == 'X'}
<div class="absolute top-4 right-4"
><CloseButton on:close={() => (open = false)} /></div
>
{/if}
<div class="flex">
<div class="text-left flex-1">
<div class="flex flex-row items-center justify-between">
<h3 class="text-emphasis text-lg font-semibold">{title}</h3>
{@render settings?.()}
</div>
<div class="mt-4 text-sm text-primary">
{@render children_render?.()}
</div>
</div>
{/if}
<div class="flex">
<div class="text-left flex-1">
<div class="flex flex-row items-center justify-between">
<h3 class="text-emphasis text-lg font-semibold">{title}</h3>
{@render settings?.()}
</div>
<div class="mt-4 text-sm text-primary">
{@render children?.()}
</div>
{#if kind == 'button'}
<div
class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4"
>
{@render actions?.()}
<Button
on:click={() => {
dispatch('canceled')
open = false
}}
color="light"
size="sm"
>
{cancelText ?? 'Cancel'}
</Button>
</div>
{/if}
</div>
</div>
{#if kind == 'button'}
<div class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4">
{@render actions?.()}
<Button
on:click={() => {
dispatch('canceled')
open = false
}}
color="light"
size="sm"
>
{cancelText ?? 'Cancel'}
</Button>
</div>
{/if}
</div>
</div>
{/if}
{/snippet}
</Disposable>
</div>
</div>
{/if}

View File

@@ -12,7 +12,7 @@
import { yamlStringifyExceptKeys } from './utils'
import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
import { getToolNameError } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import {
inputBaseClass,
inputBorderClass,
@@ -117,7 +117,6 @@ Generate a tool name for the script below:
elementProps?: Record<string, any>
class?: string
onChange?: (content: string) => void
siblingToolNames?: string[]
}
let {
@@ -131,16 +130,9 @@ Generate a tool name for the script below:
elementType = 'input',
elementProps = {},
class: clazz = '',
onChange = undefined,
siblingToolNames = undefined
onChange = undefined
}: Props = $props()
let toolNameError = $derived(
promptConfigName === 'agentToolFunctionName'
? getToolNameError(content ?? '', undefined, siblingToolNames)
: undefined
)
let el: HTMLElement | undefined = $state()
let generatedContent = $state('')
let active = $state(false)
@@ -355,16 +347,16 @@ Generate a tool name for the script below:
inputBaseClass,
inputSizeClasses.md,
inputBorderClass({
error: !!toolNameError
error: promptConfigName === 'agentToolFunctionName' && !validateToolName(content ?? '')
}),
'w-full'
)}
onfocus={() => (focused = true)}
onblur={() => (focused = false)}
/>
{#if toolNameError}
{#if promptConfigName === 'agentToolFunctionName' && !validateToolName(content ?? '')}
<p class="text-3xs text-red-400 leading-tight mt-0.5">
{toolNameError}
Invalid tool name, should only contain letters, numbers and underscores
</p>
{/if}
{/if}

View File

@@ -30,37 +30,11 @@
fontClass?: string
} = $props()
// Check if an originalType like "string | string[]" is a top-level
// union where at least one member is an array type (ends with "[]").
// Splits on "|" only at the top level (not inside {}, <>, or ()).
function isUnionWithArray(originalType: string | undefined): boolean {
if (!originalType) return false
let depth = 0
const parts: string[] = []
let cur = ''
for (const ch of originalType) {
if (ch === '{' || ch === '<' || ch === '(') depth++
else if (ch === '}' || ch === '>' || ch === ')') depth--
else if (ch === '|' && depth === 0) {
parts.push(cur.trim())
cur = ''
continue
}
cur += ch
}
parts.push(cur.trim())
// Match TS array syntax (T[]) and Python list syntax (list[T] / List[T])
return parts.length > 1 && parts.some((p) => p.endsWith('[]') || /^[Ll]ist\[.+\]$/.test(p))
}
// Get list of arguments eligible for accumulation from schema.
// Includes array-type arguments and union types like T | T[]
// whose scalar values are wrapped into single-element arrays
// at aggregation time.
// Get list of array-type arguments from schema
let arrayArgs = $derived(
schema?.properties
? Object.entries(schema.properties)
.filter(([_, prop]) => prop.type === 'array' || isUnionWithArray(prop.originalType))
.filter(([_, prop]) => prop.type === 'array')
.map(([key, _]) => key)
: []
)
@@ -137,8 +111,8 @@
<Label label="Argument to accumulate (optional)">
{#snippet header()}
<Tooltip>
Select a list-type argument to accumulate across debounced executions. Values from each
debounced execution will be appended together.</Tooltip
Select a list-type argument to accumulate across debounced executions. Values from
each debounced execution will be appended together.</Tooltip
>
{/snippet}
<select disabled={!$enterpriseLicense} bind:value={selectedArg}>
@@ -157,8 +131,8 @@
<Label label="Max total debouncing time (optional)">
{#snippet header()}
<Tooltip>
Maximum total time (in seconds) that a job can be debounced before it must execute. Once
this time is reached, the job will run regardless of ongoing debouncing.</Tooltip
Maximum total time (in seconds) that a job can be debounced before it must execute.
Once this time is reached, the job will run regardless of ongoing debouncing.</Tooltip
>
{/snippet}
<SecondsInput disabled={!$enterpriseLicense} bind:seconds={max_total_debouncing_time} />
@@ -166,16 +140,11 @@
<Label label="Max total debounces amount (optional)">
{#snippet header()}
<Tooltip>
Maximum number of times a job can be debounced before it must execute. Once this count
is reached, the job will run regardless of ongoing debouncing.</Tooltip
Maximum number of times a job can be debounced before it must execute. Once this
count is reached, the job will run regardless of ongoing debouncing.</Tooltip
>
{/snippet}
<input
type="number"
disabled={!$enterpriseLicense}
bind:value={max_total_debounces_amount}
min="0"
/>
<input type="number" disabled={!$enterpriseLicense} bind:value={max_total_debounces_amount} min="0" />
</Label>
</div>
{/if}

View File

@@ -12,7 +12,6 @@
action?: import('svelte').Snippet
children?: import('svelte').Snippet
isAgentTool?: boolean
siblingToolNames?: string[]
}
let {
@@ -24,8 +23,7 @@
header,
action,
children,
isAgentTool = false,
siblingToolNames = undefined
isAgentTool = false
}: Props = $props()
</script>
@@ -40,7 +38,6 @@
{flowModuleValue}
{action}
{isAgentTool}
{siblingToolNames}
>
{@render header?.()}
</FlowCardHeader>

View File

@@ -18,7 +18,7 @@
import { Flag, Lock, RefreshCw, Unlock } from 'lucide-svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { getToolNameError } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import { DEFAULT_HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION } from '$lib/hub'
interface Props {
@@ -28,7 +28,6 @@
children?: import('svelte').Snippet
action?: import('svelte').Snippet
isAgentTool?: boolean
siblingToolNames?: string[]
}
let {
@@ -37,14 +36,9 @@
summary = $bindable(undefined),
children,
action,
isAgentTool = false,
siblingToolNames = undefined
isAgentTool = false
}: Props = $props()
let toolNameError = $derived(
isAgentTool ? getToolNameError(summary ?? '', undefined, siblingToolNames) : undefined
)
let latestHash: string | undefined = $state(undefined)
// Extract version_id from hub path (format: hub/{version_id}/{app}/{summary})
@@ -109,7 +103,6 @@
elementProps={{
placeholder: isAgentTool ? 'Tool name' : 'Summary'
}}
{siblingToolNames}
/>
{:else if flowModuleValue.type === 'script' && 'path' in flowModuleValue && flowModuleValue.path}
<IconedPath path={flowModuleValue.path} hash={flowModuleValue.hash} class="grow" />
@@ -180,16 +173,14 @@
/>
</div>
{/if}
<div class="flex flex-col w-full grow">
<input
bind:value={summary}
placeholder={isAgentTool ? 'Tool name' : 'Summary'}
class={twMerge('w-full grow', toolNameError && '!border-red-400')}
/>
{#if toolNameError}
<p class="text-3xs text-red-400 leading-tight mt-0.5">{toolNameError}</p>
{/if}
</div>
<input
bind:value={summary}
placeholder={isAgentTool ? 'Tool name' : 'Summary'}
class={twMerge(
'w-full grow',
isAgentTool && !validateToolName(summary ?? '') && '!border-red-400'
)}
/>
{:else if flowModuleValue.type === 'flow'}
<Badge color="indigo" capitalize>flow</Badge>
<input bind:value={summary} placeholder="Summary" class="w-full grow" />

View File

@@ -14,7 +14,6 @@
previousModule?: FlowModule | undefined
forceTestTab?: Record<string, boolean>
highlightArg?: Record<string, string | undefined>
siblingToolNames?: string[]
}
let {
@@ -24,8 +23,7 @@
parentModule = undefined,
previousModule = undefined,
forceTestTab,
highlightArg,
siblingToolNames = undefined
highlightArg
}: Props = $props()
</script>
@@ -45,7 +43,6 @@
forceTestTab={forceTestTab?.[tool.id]}
highlightArg={highlightArg?.[tool.id]}
isAgentTool={true}
{siblingToolNames}
/>
{:else if isMcpTool(tool)}
<!-- MCP tool - use McpToolEditor -->

View File

@@ -101,7 +101,9 @@
)
let canMoveSelected = $derived(
resolvedModuleIds.length > 0 &&
areContiguousSiblings(locateModules(resolvedModuleIds, flowStore.val.value.modules ?? []))
areContiguousSiblings(
locateModules(resolvedModuleIds, flowStore.val.value.modules ?? [])
)
)
</script>

View File

@@ -110,7 +110,6 @@
forceTestTab?: boolean
highlightArg?: string
isAgentTool?: boolean
siblingToolNames?: string[]
}
let {
@@ -126,8 +125,7 @@
savedModule = undefined,
forceTestTab = false,
highlightArg = undefined,
isAgentTool = false,
siblingToolNames = undefined
isAgentTool = false
}: Props = $props()
let workspaceScriptTag: string | undefined = $state(undefined)
@@ -239,9 +237,7 @@
}
let forceReload = $state(0)
let editorPanelSize = $state(
untrack(() => noEditor) ? 0 : flowModule.value.type == 'script' ? 30 : 50
)
let editorPanelSize = $state(untrack(() => noEditor) ? 0 : flowModule.value.type == 'script' ? 30 : 50)
let editorSettingsPanelSize = $state(100 - untrack(() => editorPanelSize))
let stepHistoryLoader = getStepHistoryLoaderContext()
@@ -730,7 +726,6 @@
}}
bind:summary={flowModule.summary}
{isAgentTool}
{siblingToolNames}
>
{#snippet header()}
<FlowModuleHeader
@@ -1067,8 +1062,8 @@
{enableAi}
{isAgentTool}
allowedAiTransforms={isAgentTool && flowModule.value.type === 'aiagent'
? ['user_message']
: undefined}
? ['user_message']
: undefined}
helperScript={retrieveDynCodeAndLang(flowModule.value)}
chatInputEnabled={flowStore.val.value?.chat_input_enabled ?? false}
/>

View File

@@ -305,7 +305,6 @@
{enableAi}
{forceTestTab}
{highlightArg}
siblingToolNames={flowModule.value.tools.map((t) => t.summary ?? '')}
/>
{/if}
{/each}

View File

@@ -15,13 +15,11 @@
let {
disableAi,
small,
diffManager,
compact = false
diffManager
}: {
small: boolean
disableAi?: boolean
diffManager?: FlowDiffManager
compact?: boolean
} = $props()
const dispatch = createEventDispatcher<{
@@ -60,8 +58,6 @@
selectionManager.selectId('failure')
refreshStateStore(flowStore)
}
const smallFailureModule = $derived(!(failureModuleId && diffManager && moduleAction) && compact)
</script>
{#if flowStore.val?.value?.failure_module}
@@ -71,7 +67,7 @@
<Button
variant="default"
unifiedSize="sm"
wrapperClasses={compact ? undefined : twMerge('min-w-36', small ? 'max-w-52' : 'max-w-64')}
wrapperClasses={twMerge('min-w-36', small ? 'max-w-52' : 'max-w-64')}
id="flow-editor-error-handler"
selected={selectionManager.getSelectedId()?.includes('failure')}
onClick={() => {
@@ -91,40 +87,26 @@
/>
{/if}
<Bug size={14} class="shrink-0" />
{#if !smallFailureModule}
<div class="truncate grow min-w-0 text-center text-xs">
{flowStore.val.value.failure_module?.summary ||
(flowStore.val.value.failure_module?.value.type === 'rawscript'
? `${flowStore.val.value.failure_module?.value.language}`
: 'TBD')}
</div>
<button
title="Delete failure script"
type="button"
class="ml-1"
onclick={() => {
flowStore.val.value.failure_module = undefined
selectionManager.selectId('settings-metadata')
}}
>
<X size={12} />
</button>
{/if}
</Button>
{#if smallFailureModule}
<div class="truncate grow min-w-0 text-center text-xs">
{flowStore.val.value.failure_module?.summary ||
(flowStore.val.value.failure_module?.value.type === 'rawscript'
? `${flowStore.val.value.failure_module?.value.language}`
: 'TBD')}
</div>
<button
title="Delete failure script"
type="button"
class="absolute -top-1.5 -right-1.5 rounded-full bg-surface border border-border p-0.5 hover:bg-surface-hover"
class="ml-1"
onclick={() => {
flowStore.val.value.failure_module = undefined
selectionManager.selectId('settings-metadata')
}}
>
<X size={10} />
<X size={12} />
</button>
{/if}
</Button>
</div>
{:else}
<!-- Index 0 is used by the tutorial to identify the first "Add step" -->
@@ -142,17 +124,14 @@
{#snippet trigger()}
<Button
unifiedSize="sm"
wrapperClasses={compact ? undefined : 'min-w-36'}
wrapperClasses="min-w-36"
title={`Add failure module`}
variant="default"
id={`flow-editor-add-step-error-handler-button`}
nonCaptureEvent
startIcon={{ icon: Bug }}
iconOnly={compact}
>
{#if !compact}
Error Handler
{/if}
Error Handler
</Button>
{/snippet}
</InsertModulePopover>

View File

@@ -271,8 +271,6 @@
let sidebarMode: 'list' | 'graph' = 'graph'
let minHeight = $state(0)
let flowPaneWidth = $state(0)
let compactTopbar = $derived(flowPaneWidth < 700)
export function selectNextId(id: any) {
if (flowStore.val.value.modules) {
@@ -507,12 +505,11 @@
{/each}
</ConfirmationModal>
</Portal>
<div class="flex flex-col h-full relative -pt-1" bind:clientWidth={flowPaneWidth}>
<div class="flex flex-col h-full relative -pt-1">
<div
class={`z-50 absolute inline-flex flex-col gap-2 top-3 left-1/2 -translate-x-1/2 flex-initial items-center transition-colors duration-[400ms] ease-linear bg-surface-100`}
>
<FlowStickyNode
compact={compactTopbar}
{disableAi}
{showFlowAiButton}
{disableSettings}

View File

@@ -21,7 +21,6 @@
toggleNoteMode?: () => void
disableAi?: boolean
diffManager?: FlowDiffManager
compact?: boolean
}
let {
@@ -34,8 +33,7 @@
noteMode,
toggleNoteMode,
disableAi,
diffManager,
compact = false
diffManager
}: Props = $props()
const { selectionManager, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
@@ -44,31 +42,23 @@
<div class="flex flex-row gap-2 p-1 rounded-md bg-surface">
{#if !disableSettings}
<Popover>
<Button
unifiedSize="sm"
wrapperClasses={compact ? undefined : 'min-w-36'}
startIcon={{ icon: Settings }}
selected={selectedId?.startsWith('settings')}
variant="default"
title="Settings"
iconOnly={compact && !flowStore.val.value.same_worker}
onClick={() => selectionManager.selectId('settings')}
>
{#if !compact}
Settings
{/if}
{#if flowStore.val.value.same_worker}
<Badge color="blue" wrapperClass="max-h-[18px]">./shared</Badge>
{/if}
</Button>
{#snippet text()}
Settings
{/snippet}
</Popover>
<Button
unifiedSize="sm"
wrapperClasses="min-w-36"
startIcon={{ icon: Settings }}
selected={selectedId?.startsWith('settings')}
variant="default"
title="Settings"
onClick={() => selectionManager.selectId('settings')}
>
Settings
{#if flowStore.val.value.same_worker}
<Badge color="blue" wrapperClass="max-h-[18px]">./shared</Badge>
{/if}
</Button>
{/if}
<Popover>
<FlowErrorHandlerItem {disableAi} small={smallErrorHandler} {compact} {diffManager} on:generateStep />
<FlowErrorHandlerItem {disableAi} small={smallErrorHandler} {diffManager} on:generateStep />
{#snippet text()}
Error Handler
{/snippet}

View File

@@ -300,7 +300,6 @@ export type AiToolN = {
data: {
tool: string
type?: string
nameError?: string
eventHandlers: GraphEventHandlers
moduleId: string
insertable: boolean

View File

@@ -1,29 +1,10 @@
<script module lang="ts">
import { forbiddenIds } from '$lib/components/flows/idUtils'
export function getToolNameError(
name: string,
type?: string,
siblingNames?: string[]
): string | undefined {
if (type === 'websearch') return undefined
if (type === 'mcp') {
return name.length > 0 ? undefined : 'Tool name must not be empty'
}
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
return 'Tool name must only contain letters, numbers and underscores'
}
if (forbiddenIds.includes(name)) {
return `'${name}' is a reserved name`
}
if (siblingNames && siblingNames.filter((n) => n === name).length > 1) {
return 'Duplicate tool name'
}
return undefined
}
export function validateToolName(name: string, type?: string) {
return getToolNameError(name, type) === undefined
if (type === 'websearch') return true
if (type === 'mcp') {
return name.length > 0
}
return /^[a-zA-Z0-9_]+$/.test(name)
}
export const AI_TOOL_BASE_OFFSET = 5
@@ -166,7 +147,6 @@
}
}
const siblingNames = tools.map((t) => t.name)
const toolNodes: (Node & AiToolN)[] = tools.map((tool, i) => {
let inputToolXGap = 12
let inputToolWidth = (ROW_WIDTH - inputToolXGap) / 2
@@ -180,7 +160,6 @@
data: {
tool: tool.name,
type: tool.type,
nameError: getToolNameError(tool.name, tool.type, siblingNames),
eventHandlers,
moduleId: tool.id,
insertable,
@@ -190,13 +169,13 @@
width: inputToolWidth,
position: {
x:
tools.length === 1
(tools.length === 1
? (ROW_WIDTH - inputToolWidth) / 2
: (i + 1) % 2 === 0
? inputToolWidth + inputToolXGap
: isLastRow && tools.length % 2 === 1
? (ROW_WIDTH - inputToolWidth) / 2
: 0,
: 0),
y:
baseOffset +
rowOffset *
@@ -308,7 +287,7 @@
const flowModuleState = $derived(data.flowModuleStates?.[data.moduleId])
let colorClasses = $derived(
getNodeColorClasses(
data.nameError ? 'Failure' : flowModuleState?.type,
!validateToolName(data.tool, data.type) ? 'Failure' : flowModuleState?.type,
selectionManager?.getSelectedId() === data.moduleId
)
)
@@ -345,7 +324,12 @@
<Wrench size={16} class="ml-1 shrink-0" />
{/if}
<span class={twMerge('text-3xs truncate flex-1', data.nameError && 'text-red-400')}>
<span
class={twMerge(
'text-3xs truncate flex-1',
!validateToolName(data.tool, data.type) && 'text-red-400'
)}
>
{data.tool || 'Missing name'}
</span>
</button>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { run } from 'svelte/legacy';
import { FlowService, ScriptService, UserService, type TruncatedToken } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
@@ -10,21 +10,23 @@
import { capitalize } from '$lib/utils'
interface Props {
isFlow: boolean
path: string
labelPrefix: 'email' | 'webhook'
isFlow: boolean;
path: string;
labelPrefix: 'email' | 'webhook';
}
let { isFlow, path, labelPrefix }: Props = $props()
let { isFlow, path, labelPrefix }: Props = $props();
const { triggersCount } = getContext<TriggerContext>('TriggerContext')
let tokens: TruncatedToken[] | undefined = $state(undefined)
export async function listTokens() {
tokens = isFlow
? await FlowService.listTokensOfFlow({ workspace: $workspaceStore!, path })
: await ScriptService.listTokensOfScript({ workspace: $workspaceStore!, path })
tokens = (
isFlow
? await FlowService.listTokensOfFlow({ workspace: $workspaceStore!, path })
: await ScriptService.listTokensOfScript({ workspace: $workspaceStore!, path })
).filter((x) => x.label && x.label.startsWith(labelPrefix + '-'))
if (labelPrefix == 'email') {
$triggersCount = { ...($triggersCount ?? {}), default_email_count: tokens?.length }
} else {
@@ -41,14 +43,14 @@
run(() => {
$workspaceStore && listTokens()
})
});
</script>
<div class="flex flex-col gap-2">
<Label label="Existing {capitalize(labelPrefix)} Tokens">
{#if tokens}
{#if tokens.length == 0}
<div class="text-xs text-secondary">No tokens with matching scopes found</div>
<div class="text-xs text-secondary">No {labelPrefix} specific tokens found</div>
{:else}
<div class="flex flex-col divide-y pt-2">
<div class="grid grid-cols-6 text-2xs items-center py-2">

View File

@@ -105,17 +105,17 @@ async function initWasmAsset() {
type InferAssetsResult =
| {
status: 'ok'
assets: AssetWithAccessType[]
sql_queries?: InferAssetsSqlQueryDetails[]
columns?: Record<string, AssetUsageAccessType>
}
status: 'ok'
assets: AssetWithAccessType[]
sql_queries?: InferAssetsSqlQueryDetails[]
columns?: Record<string, AssetUsageAccessType>
}
| {
status: 'error'
error: string
assets?: undefined
sql_queries?: undefined
}
status: 'error'
error: string
assets?: undefined
sql_queries?: undefined
}
export type InferAssetsSqlQueryDetails = {
query_string: string // SQL query with $1 placeholders for interpolations
@@ -384,11 +384,6 @@ export async function inferArgs(
argSigToJsonSchemaType(arg.typ, schema.properties[arg.name])
// For T | T[] detection for debouncing arg accumulation
if ((arg as any).otyp && (arg as any).otyp.includes('[') && (arg as any).otyp.includes('|')) {
schema.properties[arg.name].originalType = (arg as any).otyp
}
schema.properties[arg.name].default = arg.default
if (!arg.has_default && !schema.required.includes(arg.name)) {

View File

@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.657.2"
wmill = ">=1.655.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"

View File

@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.657.2
version: 1.655.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel

View File

@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.657.2'
ModuleVersion = '1.655.0'
# Supported PSEditions
# CompatiblePSEditions = @()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.657.2"
version = "1.655.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"

401
scripts/wm-cursor Executable file
View File

@@ -0,0 +1,401 @@
#!/usr/bin/env zsh
emulate -L zsh
setopt err_exit no_unset pipe_fail
# wm-cursor: Manage Cursor SSH remote windows with grouped tmux sessions
# Each worktree gets its own Cursor window with an independently-focused
# grouped tmux session, sharing the same window list in the status bar.
# --- Resolve script path (must be at top level, not inside a function) ---
local script_path=${0:A}
# --- Lazy Cursor CLI resolution (only when needed) ---
local cursor_bin=
resolve_cursor_cli() {
[[ -n $cursor_bin ]] && return 0
local -a cursor_bins=(~/.cursor-server/cli/servers/*/server/bin/remote-cli/cursor(NOm))
if (( ${#cursor_bins} == 0 )); then
print -u2 "Error: Cursor remote CLI not found in ~/.cursor-server/cli/servers/"
exit 1
fi
cursor_bin=${cursor_bins[1]}
# Refresh Cursor IPC socket (tmux may hold a stale one)
# Multiple stale sockets may exist; probe to find a live one
local sock
for sock in /tmp/vscode-ipc-*.sock(NOm); do
if timeout 2 env VSCODE_IPC_HOOK_CLI=$sock $cursor_bin --status &>/dev/null; then
export VSCODE_IPC_HOOK_CLI=$sock
break
fi
done
}
# --- Helper functions ---
ensure_tmux() {
if [[ -z ${TMUX-} ]]; then
print -u2 "Error: Not inside a tmux session"
exit 1
fi
}
check_dev_db() {
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^windmill-db-dev$'; then
print -u2 "Warning: windmill-db-dev container is not running"
fi
}
setup_grouped_session() {
local handle=$1 worktree_path=$2
local session_name=cursor-${handle}
# Detect the current main tmux session
local main_session=$(tmux display-message -p '#S')
# Create grouped session (shares windows with the main session)
if ! tmux has-session -t $session_name 2>/dev/null; then
tmux new-session -d -t $main_session -s $session_name
fi
# Focus on the worktree's window
tmux select-window -t ${session_name}:wm-${handle} 2>/dev/null || true
# Write .vscode/settings.json in the worktree if it doesn't already exist
local settings_file=${worktree_path}/.vscode/settings.json
if [[ ! -f $settings_file ]]; then
mkdir -p ${settings_file:h}
# Read ports from .env.local if available
local env_file=${worktree_path}/.env.local
local ports_config=""
if [[ -f $env_file ]]; then
local backend_port frontend_port
source $env_file
backend_port=${BACKEND_PORT-}
frontend_port=${FRONTEND_PORT-}
if [[ -n $backend_port && -n $frontend_port ]]; then
ports_config=',
"remote.autoForwardPorts": true,
"remote.otherPortsAttributes": {
"onAutoForward": "ignore"
},
"remote.portsAttributes": {
"'$backend_port'": { "label": "Backend", "onAutoForward": "silent" },
"'$frontend_port'": { "label": "Frontend", "onAutoForward": "openBrowserOnce" }
}'
fi
fi
cat > $settings_file <<SETTINGS
{
"rust-analyzer.initializeStopped": true,
"terminal.integrated.defaultProfile.linux": "wm-tmux",
"terminal.integrated.profiles.linux": {
"wm-tmux": {
"path": "tmux",
"args": ["attach-session", "-t", "${session_name}"]
}
}${ports_config}
}
SETTINGS
print "Created ${settings_file}"
fi
}
# --- Feature flag parsing ---
# Extracts --features <value> from args, exports CARGO_FEATURES, returns remaining args.
# Usage: parse_features_flag "$@"; set -- "${remaining_args[@]}"
parse_flags() {
remaining_args=()
while (( $# )); do
case $1 in
--features)
if (( $# < 2 )); then
print -u2 "Error: --features requires a value"
exit 1
fi
export CARGO_FEATURES=$2
shift 2
;;
--features=*)
export CARGO_FEATURES=${1#--features=}
shift
;;
--clone-db)
export WM_CLONE_DB=1
shift
;;
*)
remaining_args+=("$1")
shift
;;
esac
done
}
# --- Subcommands ---
cmd_add() {
resolve_cursor_cli
ensure_tmux
check_dev_db
parse_flags "$@"
set -- "${remaining_args[@]}"
# Snapshot worktree list before
local -a before=("${(@f)$(git worktree list --porcelain | grep '^worktree ')}")
workmux add -b "$@"
# Diff to find the new entry
local -a after=("${(@f)$(git worktree list --porcelain | grep '^worktree ')}")
local -a new=(${after:|before})
if (( ${#new} == 0 )); then
print -u2 "Error: Could not detect new worktree path"
exit 1
fi
local new_path=${new[1]#worktree }
local handle=${new_path:t}
print "New worktree: ${handle} at ${new_path}"
setup_grouped_session $handle $new_path
$cursor_bin -n $new_path
print "Opened Cursor for ${handle}"
}
cmd_open() {
local name=${1:?Usage: wm-cursor open <name>}
shift
resolve_cursor_cli
ensure_tmux
check_dev_db
parse_flags "$@"
set -- "${remaining_args[@]}"
# Write CARGO_FEATURES to .env.local if specified
if [[ -n ${CARGO_FEATURES-} ]]; then
local wt_env=$(workmux path $name)/.env.local
if [[ -f $wt_env ]]; then
# Remove existing CARGO_FEATURES line and append new one
sed -i '/^CARGO_FEATURES=/d' $wt_env
echo "CARGO_FEATURES=$CARGO_FEATURES" >> $wt_env
fi
fi
local wt_path=$(workmux path $name)
local prev_target=$(tmux display-message -p '#{session_name}:#{window_index}')
workmux open $name "$@"
tmux select-window -t $prev_target
setup_grouped_session $name $wt_path
$cursor_bin -n $wt_path
print "Opened Cursor for ${name}"
}
cmd_close() {
local name=${1:?Usage: wm-cursor close <name>}
tmux kill-session -t cursor-${name} 2>/dev/null || true
workmux close $name
}
cmd_open_ee() {
local name=${1:?Usage: wm-cursor open-ee <name>}
resolve_cursor_cli
local wt_path=$(workmux path $name)
local main_repo_root="$(cd "$(git -C "$wt_path" rev-parse --git-common-dir 2>/dev/null)/.." && pwd)"
# Find ee repo (same discovery logic as worktree-env)
local ee_repo="" candidate
for candidate in \
"${main_repo_root:+${main_repo_root}/../windmill-ee-private}" \
"${wt_path}/../windmill-ee-private" \
"${HOME}/windmill-ee-private" \
"${HOME}/projects/windmill-ee-private"; do
if [[ -n $candidate ]] && [[ -d $candidate ]]; then
ee_repo=${candidate:A}
break
fi
done
if [[ -z $ee_repo ]]; then
print -u2 "Error: Could not find windmill-ee-private repo"
exit 1
fi
local ee_worktree_dir="${ee_repo}__worktrees/${name}"
if [[ ! -d $ee_worktree_dir ]]; then
print -u2 "Error: EE worktree not found at ${ee_worktree_dir}"
exit 1
fi
$cursor_bin -n $ee_worktree_dir
print "Opened Cursor for EE worktree: ${ee_worktree_dir}"
}
cmd_setup() {
local repo_root=${1:?Usage: wm-cursor setup <repo-root>}
repo_root=${repo_root:A}
local vscode_dir=${repo_root}/.vscode
mkdir -p $vscode_dir
# --- tasks.json ---
local tasks_file=${vscode_dir}/tasks.json
local write_tasks=true
if [[ -f $tasks_file ]]; then
print -n "tasks.json already exists. Overwrite? [y/N] "
read -q || { print; write_tasks=false }
print
fi
if $write_tasks; then
cat > $tasks_file <<'TASKS'
{
"version": "2.0.0",
"tasks": [
{
"label": "Start dev DB",
"type": "shell",
"command": "./start-dev-db.sh",
"options": { "shell": { "executable": "/bin/bash" } },
"runOptions": { "runOn": "folderOpen" },
"presentation": { "reveal": "silent", "close": true },
"problemMatcher": []
}
]
}
TASKS
print "Wrote ${tasks_file}"
fi
# --- settings.json (merge wm-cursor keys, preserve existing) ---
local settings_file=${vscode_dir}/settings.json
local wmc_settings='
{
"rust-analyzer.initializeStopped": true,
"terminal.integrated.defaultProfile.linux": "wm-tmux",
"terminal.integrated.profiles.linux": {
"wm-tmux": {
"path": "tmux",
"args": ["new-session", "-A", "-s", "main"]
}
},
"remote.autoForwardPorts": true,
"remote.otherPortsAttributes": {
"onAutoForward": "ignore"
},
"remote.portsAttributes": {
"8000": { "label": "Backend", "onAutoForward": "silent" },
"3000": { "label": "Frontend", "onAutoForward": "openBrowserOnce" },
"5432": { "label": "PostgreSQL", "onAutoForward": "silent" }
}
}'
if [[ -f $settings_file ]]; then
# Strip // comments so jq can parse, merge, then write back
local existing
existing=$(python3 -c '
import json, re, sys
text = sys.stdin.read()
# Remove // comments only outside of strings
text = re.sub(r'"'"'("(?:[^"\\]|\\.)*")|//[^\n]*'"'"', lambda m: m.group(1) or "", text)
json.dump(json.loads(text), sys.stdout, indent=2)
' < $settings_file)
jq --argjson wmc "$wmc_settings" '. * $wmc' <<< "$existing" > ${settings_file}.tmp \
&& mv ${settings_file}.tmp $settings_file
print "Merged wm-cursor settings into ${settings_file}"
else
jq . <<< "$wmc_settings" > $settings_file
print "Created ${settings_file}"
fi
# --- zsh alias + completion ---
local rc=${ZDOTDIR:-$HOME}/.zshrc
local alias_line="alias wmc=${(q)script_path}"
if [[ -f $rc ]] && grep -qF 'alias wmc=' $rc; then
sed -i "s|^alias wmc=.*|${alias_line}|" $rc
print "Updated wmc alias in ${rc}"
else
print "\n# wm-cursor alias\n${alias_line}" >> $rc
print "Added wmc alias to ${rc}"
fi
local eval_line='eval "$(wmc completions)"'
if ! grep -qF 'wmc completions' $rc; then
print "${eval_line}" >> $rc
print "Added completions eval to ${rc}"
fi
}
cmd_completions() {
cat <<'COMP'
_wmc_worktree_names() {
local -a names
names=(${(f)"$(git worktree list --porcelain 2>/dev/null | sed -n 's|^worktree .*/||p' | tail -n +2)"})
_describe 'worktree' names
}
_wmc() {
local -a subcmds=(
'add:Create worktree + open Cursor'
'open:Open Cursor for existing worktree'
'open-ee:Open EE worktree in Cursor'
'close:Clean up grouped tmux session'
'setup:Set up .vscode settings, tasks + wmc alias'
'completions:Print zsh completions'
)
if (( CURRENT == 2 )); then
_describe 'subcommand' subcmds
else
case $words[2] in
open|open-ee|close)
_wmc_worktree_names
;;
esac
fi
}
compdef _wmc wmc wm-cursor
COMP
}
# --- Main ---
case ${1-} in
add) shift; cmd_add "$@" ;;
open) shift; cmd_open "$@" ;;
open-ee) shift; cmd_open_ee "$@" ;;
close) shift; cmd_close "$@" ;;
setup) shift; cmd_setup "$@" ;;
completions) cmd_completions ;;
*)
print -u2 "Usage: wm-cursor <add|open|open-ee|close|setup|completions> [args...]"
print -u2 ""
print -u2 "Subcommands:"
print -u2 " add [--features <f>] [workmux-add-args...] Create worktree + open Cursor"
print -u2 " open <name> [--features <f>] Open Cursor for existing worktree"
print -u2 " open-ee <name> Open EE worktree in Cursor"
print -u2 " close <name> Clean up grouped tmux session"
print -u2 " setup <repo-root> Set up .vscode settings, tasks + wmc alias"
print -u2 " completions Print zsh completions (use with eval)"
print -u2 ""
print -u2 "Options:"
print -u2 " --features <features> Cargo features for the backend (e.g. \"enterprise,parquet\")"
print -u2 " --clone-db Clone the main 'windmill' database instead of creating an empty one"
exit 1
;;
esac

View File

@@ -80,13 +80,6 @@ wm_copy_dependencies() {
&& echo "CLI deps installed and client generated" \
|| echo "WARNING: CLI setup failed" >&2
fi
local nav_bin="${main_repo_root}/wm-ts-nav/target/release/wm-ts-nav"
if [[ -f "$nav_bin" ]]; then
mkdir -p "${repo_root}/wm-ts-nav/target/release"
cp "$nav_bin" "${repo_root}/wm-ts-nav/target/release/"
echo "Copied wm-ts-nav binary"
fi
}
wm_allow_direnv() {
@@ -268,4 +261,5 @@ wm_shared_pre_remove() {
fi
fi
tmux kill-session -t "cursor-${wt_basename}" 2>/dev/null || true
}

View File

@@ -119,23 +119,6 @@ folder related commands
- `folder add-missing` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
- `-y, --yes` - skip confirmation prompt
### generate-metadata
Generate metadata (locks, schemas) for all scripts, flows, and apps
**Arguments:** `[folder:string]`
**Options:**
- `--yes` - Skip confirmation prompt
- `--dry-run` - Show what would be updated without making changes
- `--lock-only` - Re-generate only the lock files
- `--schema-only` - Re-generate only script schemas (skips flows and apps)
- `--skip-scripts` - Skip processing scripts
- `--skip-flows` - Skip processing flows
- `--skip-apps` - Skip processing apps
- `-i --includes <patterns:file[]>` - Comma separated patterns to specify which files to include
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which files to exclude
### gitsync-settings
Manage git-sync settings between local wmill.yaml and Windmill backend

View File

@@ -2,10 +2,10 @@
## CLI Commands
Create a folder ending with `__flow` and add a `flow.yaml` file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key. Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).
Create a folder ending with `.flow` and add a YAML file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key.
After writing, tell the user they can run:
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow__flow --yes`)
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow.flow --yes`)
- `wmill sync push` - Deploy to Windmill
Do NOT run these commands yourself. Instead, inform the user that they should run them.

View File

@@ -33,10 +33,10 @@ export const FLOW_BASE = `# Windmill Flow Building Guide
## CLI Commands
Create a folder ending with \`__flow\` and add a \`flow.yaml\` file with the flow definition.
For rawscript modules, use \`!inline path/to/script.ts\` for the content key. Inline script files should NOT include \`.inline_script.\` in their names (e.g. use \`a.ts\`, not \`a.inline_script.ts\`).
Create a folder ending with \`.flow\` and add a YAML file with the flow definition.
For rawscript modules, use \`!inline path/to/script.ts\` for the content key.
After writing, tell the user they can run:
- \`wmill flow generate-locks <path_to_flow_folder> --yes\` - Generate lock files for the specific flow you modified (e.g. \`wmill flow generate-locks f/my_folder/my_flow__flow --yes\`)
- \`wmill flow generate-locks <path_to_flow_folder> --yes\` - Generate lock files for the specific flow you modified (e.g. \`wmill flow generate-locks f/my_folder/my_flow.flow --yes\`)
- \`wmill sync push\` - Deploy to Windmill
Do NOT run these commands yourself. Instead, inform the user that they should run them.
@@ -1493,23 +1493,6 @@ folder related commands
- \`folder add-missing\` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
- \`-y, --yes\` - skip confirmation prompt
### generate-metadata
Generate metadata (locks, schemas) for all scripts, flows, and apps
**Arguments:** \`[folder:string]\`
**Options:**
- \`--yes\` - Skip confirmation prompt
- \`--dry-run\` - Show what would be updated without making changes
- \`--lock-only\` - Re-generate only the lock files
- \`--schema-only\` - Re-generate only script schemas (skips flows and apps)
- \`--skip-scripts\` - Skip processing scripts
- \`--skip-flows\` - Skip processing flows
- \`--skip-apps\` - Skip processing apps
- \`-i --includes <patterns:file[]>\` - Comma separated patterns to specify which files to include
- \`-e --excludes <patterns:file[]>\` - Comma separated patterns to specify which files to exclude
### gitsync-settings
Manage git-sync settings between local wmill.yaml and Windmill backend

View File

@@ -124,23 +124,6 @@ folder related commands
- `folder add-missing` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
- `-y, --yes` - skip confirmation prompt
### generate-metadata
Generate metadata (locks, schemas) for all scripts, flows, and apps
**Arguments:** `[folder:string]`
**Options:**
- `--yes` - Skip confirmation prompt
- `--dry-run` - Show what would be updated without making changes
- `--lock-only` - Re-generate only the lock files
- `--schema-only` - Re-generate only script schemas (skips flows and apps)
- `--skip-scripts` - Skip processing scripts
- `--skip-flows` - Skip processing flows
- `--skip-apps` - Skip processing apps
- `-i --includes <patterns:file[]>` - Comma separated patterns to specify which files to include
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which files to exclude
### gitsync-settings
Manage git-sync settings between local wmill.yaml and Windmill backend

View File

@@ -18,7 +18,7 @@ This interactive command creates a complete app structure with your choice of fr
## App Structure
```
my_app__raw_app/
my_app.raw_app/
├── AGENTS.md # AI agent instructions (auto-generated)
├── DATATABLES.md # Database schemas (run 'wmill app generate-agents' to refresh)
├── raw_app.yaml # App configuration (summary, path, data settings)

View File

@@ -7,10 +7,10 @@ description: MUST use when creating flows.
## CLI Commands
Create a folder ending with `__flow` and add a `flow.yaml` file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key. Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).
Create a folder ending with `.flow` and add a YAML file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key.
After writing, tell the user they can run:
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow__flow --yes`)
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow.flow --yes`)
- `wmill sync push` - Deploy to Windmill
Do NOT run these commands yourself. Instead, inform the user that they should run them.

View File

@@ -2,10 +2,10 @@
## CLI Commands
Create a folder ending with `__flow` and add a `flow.yaml` file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key. Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).
Create a folder ending with `.flow` and add a YAML file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key.
After writing, tell the user they can run:
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow__flow --yes`)
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow.flow --yes`)
- `wmill sync push` - Deploy to Windmill
Do NOT run these commands yourself. Instead, inform the user that they should run them.

View File

@@ -13,7 +13,7 @@ This interactive command creates a complete app structure with your choice of fr
## App Structure
```
my_app__raw_app/
my_app.raw_app/
├── AGENTS.md # AI agent instructions (auto-generated)
├── DATATABLES.md # Database schemas (run 'wmill app generate-agents' to refresh)
├── raw_app.yaml # App configuration (summary, path, data settings)

View File

@@ -1094,20 +1094,6 @@ export function getFlowPrompt(): string {
# Generate skills TypeScript export for CLI
skills_ts = generate_skills_ts_export(skills, schema_yaml_content)
# Replace hardcoded path conventions with placeholders for CLI runtime resolution.
# init.ts resolves these based on the nonDottedPaths setting in wmill.yaml.
# (Frontend auto-generated files keep the default non-dotted conventions.)
skills_ts = (skills_ts
.replace("\\`__flow\\`", "\\`{{FLOW_SUFFIX}}\\`")
.replace(
"Inline script files should NOT include \\`.inline_script.\\`"
" in their names (e.g. use \\`a.ts\\`, not \\`a.inline_script.ts\\`).",
"{{INLINE_SCRIPT_NAMING}}"
)
.replace("my_flow__flow", "my_flow{{FLOW_SUFFIX}}")
.replace("my_app__raw_app/", "my_app{{RAW_APP_SUFFIX}}/")
)
(CLI_GUIDANCE_DIR / "skills.ts").write_text(skills_ts)
print(f"\nGenerated files:")

View File

@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.657.2",
"version": "1.655.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]

View File

@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.657.2",
"version": "1.655.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"sideEffects": false,

View File

@@ -1 +1 @@
1.657.2
1.655.0

620
wm-ts-nav/Cargo.lock generated
View File

@@ -1,620 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "bstr"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "cc"
version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "globset"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "ignore"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rusqlite"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tree-sitter"
version = "0.24.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5387dffa7ffc7d2dae12b50c6f7aab8ff79d6210147c6613561fc3d474c6f75"
dependencies = [
"cc",
"regex",
"regex-syntax",
"streaming-iterator",
"tree-sitter-language",
]
[[package]]
name = "tree-sitter-language"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
[[package]]
name = "tree-sitter-rust"
version = "0.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca8ccb3e3a3495c8a943f6c3fd24c3804c471fd7f4f16087623c7fa4c0068e8a"
dependencies = [
"cc",
"tree-sitter-language",
]
[[package]]
name = "tree-sitter-typescript"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff"
dependencies = [
"cc",
"tree-sitter-language",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wm-ts-nav"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"ignore",
"rayon",
"rusqlite",
"serde",
"serde_json",
"tree-sitter",
"tree-sitter-rust",
"tree-sitter-typescript",
]
[[package]]
name = "zerocopy"
version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@@ -1,21 +0,0 @@
[package]
name = "wm-ts-nav"
version = "0.1.0"
edition = "2021"
[dependencies]
tree-sitter = "0.24"
tree-sitter-rust = "0.23"
tree-sitter-typescript = "0.23"
rusqlite = { version = "0.32", features = ["bundled"] }
rayon = "1.10"
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
ignore = "0.4"
[profile.release]
opt-level = 2
lto = "thin"

View File

@@ -1,10 +0,0 @@
#!/bin/sh
# Auto-rebuilding wrapper for wm-ts-nav
DIR="$(cd "$(dirname "$0")" && pwd)"
BIN="$DIR/target/release/wm-ts-nav"
if [ ! -f "$BIN" ] || [ -n "$(find "$DIR/src" "$DIR/Cargo.toml" -newer "$BIN" 2>/dev/null | head -1)" ]; then
cargo build --release --manifest-path "$DIR/Cargo.toml" >&2 || exit 1
fi
exec "$BIN" "$@"

View File

@@ -1,422 +0,0 @@
use anyhow::{Context, Result};
use rusqlite::{params, Connection};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::parser::{IdentRef, Symbol};
pub struct Db {
conn: Connection,
}
impl Db {
pub fn open(cache_dir: &Path) -> Result<Self> {
std::fs::create_dir_all(cache_dir)
.with_context(|| format!("creating cache dir: {}", cache_dir.display()))?;
let db_path = cache_dir.join("index.db");
let conn = Connection::open(&db_path)
.with_context(|| format!("opening db: {}", db_path.display()))?;
conn.execute_batch(
"PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY,
path TEXT NOT NULL UNIQUE,
mtime_secs INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS symbols (
id INTEGER PRIMARY KEY,
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
name TEXT NOT NULL,
kind TEXT NOT NULL,
line INTEGER NOT NULL,
end_line INTEGER NOT NULL,
signature TEXT,
parent TEXT
);
CREATE TABLE IF NOT EXISTS refs (
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
name TEXT NOT NULL,
line INTEGER NOT NULL,
import_path TEXT
);
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id);
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);
CREATE INDEX IF NOT EXISTS idx_refs_name ON refs(name);
CREATE INDEX IF NOT EXISTS idx_refs_file ON refs(file_id);",
)?;
Ok(Self { conn })
}
pub fn begin(&self) -> Result<()> {
self.conn.execute_batch("BEGIN")?;
Ok(())
}
pub fn commit(&self) -> Result<()> {
self.conn.execute_batch("COMMIT")?;
Ok(())
}
pub fn upsert_file(
&self,
path: &str,
mtime_secs: i64,
symbols: &[Symbol],
refs: &[IdentRef],
) -> Result<()> {
// Delete old entry if exists
self.conn.execute(
"DELETE FROM refs WHERE file_id IN (SELECT id FROM files WHERE path = ?1)",
params![path],
)?;
self.conn.execute(
"DELETE FROM symbols WHERE file_id IN (SELECT id FROM files WHERE path = ?1)",
params![path],
)?;
self.conn
.execute("DELETE FROM files WHERE path = ?1", params![path])?;
// Insert new file
self.conn.execute(
"INSERT INTO files (path, mtime_secs) VALUES (?1, ?2)",
params![path, mtime_secs],
)?;
let file_id = self.conn.last_insert_rowid();
// Insert symbols
let mut stmt = self.conn.prepare_cached(
"INSERT INTO symbols (file_id, name, kind, line, end_line, signature, parent) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
)?;
for sym in symbols {
stmt.execute(params![
file_id,
sym.name,
sym.kind,
sym.line,
sym.end_line,
sym.signature,
sym.parent,
])?;
}
// Insert refs
let mut ref_stmt = self.conn.prepare_cached(
"INSERT INTO refs (file_id, name, line, import_path) VALUES (?1, ?2, ?3, ?4)",
)?;
for r in refs {
ref_stmt.execute(params![file_id, r.name, r.line, r.import_path])?;
}
Ok(())
}
pub fn remove_file(&self, path: &str) -> Result<()> {
self.conn.execute(
"DELETE FROM refs WHERE file_id IN (SELECT id FROM files WHERE path = ?1)",
params![path],
)?;
self.conn.execute(
"DELETE FROM symbols WHERE file_id IN (SELECT id FROM files WHERE path = ?1)",
params![path],
)?;
self.conn
.execute("DELETE FROM files WHERE path = ?1", params![path])?;
Ok(())
}
pub fn all_indexed_paths(&self) -> Result<Vec<(String, i64)>> {
let mut stmt = self
.conn
.prepare("SELECT path, mtime_secs FROM files")?;
let rows = stmt
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn search_symbols(
&self,
pattern: &str,
kind_filter: Option<&str>,
parent_filter: Option<&str>,
limit: usize,
) -> Result<Vec<SearchResult>> {
let mut conditions = vec!["s.name LIKE ?1".to_string()];
if let Some(kind) = kind_filter {
conditions.push(format!("s.kind = '{kind}'"));
}
if let Some(parent) = parent_filter {
conditions.push(format!("s.parent LIKE '%{parent}%'"));
}
let where_clause = conditions.join(" AND ");
let query = format!(
"SELECT s.name, s.kind, s.line, s.end_line, s.signature, s.parent, f.path
FROM symbols s JOIN files f ON s.file_id = f.id
WHERE {where_clause}
ORDER BY s.name LIMIT ?2"
);
let like_pattern = if pattern.contains('%') || pattern.contains('_') {
pattern.to_string()
} else {
format!("%{pattern}%")
};
let mut stmt = self.conn.prepare(&query)?;
let rows = stmt
.query_map(params![like_pattern, limit as i64], |row| {
Ok(SearchResult {
name: row.get(0)?,
kind: row.get(1)?,
line: row.get(2)?,
end_line: row.get(3)?,
signature: row.get(4)?,
parent: row.get(5)?,
path: row.get(6)?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn file_symbols(&self, path: &str) -> Result<Vec<SearchResult>> {
let mut stmt = self.conn.prepare(
"SELECT s.name, s.kind, s.line, s.end_line, s.signature, s.parent, f.path
FROM symbols s JOIN files f ON s.file_id = f.id
WHERE f.path = ?1
ORDER BY s.line",
)?;
let rows = stmt
.query_map(params![path], |row| {
Ok(SearchResult {
name: row.get(0)?,
kind: row.get(1)?,
line: row.get(2)?,
end_line: row.get(3)?,
signature: row.get(4)?,
parent: row.get(5)?,
path: row.get(6)?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn find_refs(
&self,
name: &str,
limit: usize,
file_filter: Option<&str>,
with_caller: bool,
) -> Result<Vec<RefResult>> {
let mut conditions = vec!["r.name = ?1".to_string()];
if let Some(file) = file_filter {
conditions.push(format!("f.path LIKE '%{}'", file.replace('\'', "''")));
}
let where_clause = conditions.join(" AND ");
if with_caller {
let query = format!(
"SELECT path, line, import_path, caller_name, caller_kind FROM (
SELECT f.path, r.line, r.import_path, s.name AS caller_name, s.kind AS caller_kind,
ROW_NUMBER() OVER (
PARTITION BY r.file_id, r.line
ORDER BY (s.end_line - s.line) ASC
) AS rn
FROM refs r
JOIN files f ON r.file_id = f.id
LEFT JOIN symbols s ON s.file_id = r.file_id
AND s.line <= r.line AND r.line <= s.end_line
AND s.kind IN ('function', 'impl', 'class', 'interface', 'method')
WHERE {where_clause}
) WHERE rn = 1
ORDER BY path, line
LIMIT ?2"
);
let mut stmt = self.conn.prepare(&query)?;
let rows = stmt
.query_map(params![name, limit as i64], |row| {
Ok(RefResult {
path: row.get(0)?,
line: row.get(1)?,
import_path: row.get(2)?,
caller_name: row.get(3)?,
caller_kind: row.get(4)?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(rows)
} else {
let query = format!(
"SELECT f.path, r.line, r.import_path
FROM refs r JOIN files f ON r.file_id = f.id
WHERE {where_clause}
ORDER BY f.path, r.line
LIMIT ?2"
);
let mut stmt = self.conn.prepare(&query)?;
let rows = stmt
.query_map(params![name, limit as i64], |row| {
Ok(RefResult {
path: row.get(0)?,
line: row.get(1)?,
import_path: row.get(2)?,
caller_name: None,
caller_kind: None,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(rows)
}
}
pub fn find_callers(&self, name: &str, limit: usize) -> Result<Vec<CallerResult>> {
let mut stmt = self.conn.prepare(
"SELECT caller_name, caller_kind, caller_line, caller_end_line, path, ref_line FROM (
SELECT s.name AS caller_name, s.kind AS caller_kind,
s.line AS caller_line, s.end_line AS caller_end_line,
f.path, r.line AS ref_line,
ROW_NUMBER() OVER (
PARTITION BY r.file_id, r.line
ORDER BY (s.end_line - s.line) ASC
) AS rn
FROM refs r
JOIN symbols s ON s.file_id = r.file_id
AND s.line <= r.line AND r.line <= s.end_line
AND s.kind IN ('function', 'impl', 'class', 'interface', 'method')
JOIN files f ON r.file_id = f.id
WHERE r.name = ?1
) WHERE rn = 1
ORDER BY path, caller_line
LIMIT ?2",
)?;
let rows = stmt
.query_map(params![name, limit as i64], |row| {
Ok(CallerResult {
caller_name: row.get(0)?,
caller_kind: row.get(1)?,
caller_line: row.get(2)?,
caller_end_line: row.get(3)?,
path: row.get(4)?,
ref_line: row.get(5)?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn find_callees(
&self,
name: &str,
kind_filter: Option<&str>,
file_filter: Option<&str>,
) -> Result<Vec<CalleeResult>> {
// First find the symbol
let results = self.search_symbols(name, kind_filter, None, 100)?;
let exact: Vec<_> = results.into_iter().filter(|r| r.name == name).collect();
if exact.is_empty() {
return Ok(vec![]);
}
let mut all_callees = Vec::new();
for sym in &exact {
if let Some(file) = file_filter {
if !sym.path.contains(file) {
continue;
}
}
let mut stmt = self.conn.prepare(
"SELECT DISTINCT r.name, r.import_path
FROM refs r
JOIN files f ON r.file_id = f.id
WHERE f.path = ?1 AND r.line >= ?2 AND r.line <= ?3
ORDER BY r.name",
)?;
let rows = stmt
.query_map(params![sym.path, sym.line, sym.end_line], |row| {
Ok(CalleeResult {
name: row.get(0)?,
import_path: row.get(1)?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
all_callees.extend(rows);
}
// Deduplicate by name
all_callees.sort_by(|a, b| a.name.cmp(&b.name));
all_callees.dedup_by(|a, b| a.name == b.name);
Ok(all_callees)
}
}
#[derive(Debug, serde::Serialize)]
pub struct RefResult {
pub path: String,
pub line: i64,
pub import_path: Option<String>,
pub caller_name: Option<String>,
pub caller_kind: Option<String>,
}
#[derive(Debug, serde::Serialize)]
pub struct CallerResult {
pub caller_name: String,
pub caller_kind: String,
pub caller_line: i64,
pub caller_end_line: i64,
pub path: String,
pub ref_line: i64,
}
#[derive(Debug, serde::Serialize)]
pub struct CalleeResult {
pub name: String,
pub import_path: Option<String>,
}
#[derive(Debug, serde::Serialize)]
pub struct SearchResult {
pub name: String,
pub kind: String,
pub line: i64,
pub end_line: i64,
pub signature: Option<String>,
pub parent: Option<String>,
pub path: String,
}
pub fn mtime_secs(path: &Path) -> Result<i64> {
let meta = std::fs::metadata(path)?;
let mtime = meta
.modified()?
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
Ok(mtime.as_secs() as i64)
}
pub fn cache_dir_for(root: &Path) -> PathBuf {
let hash = {
let s = root.to_string_lossy();
let mut h: u64 = 5381;
for b in s.bytes() {
h = h.wrapping_mul(33).wrapping_add(b as u64);
}
h
};
dirs_cache().join(format!("{hash:x}"))
}
fn dirs_cache() -> PathBuf {
if let Ok(d) = std::env::var("XDG_CACHE_HOME") {
PathBuf::from(d).join("wm-ts-nav")
} else if let Ok(d) = std::env::var("HOME") {
PathBuf::from(d).join(".cache").join("wm-ts-nav")
} else {
PathBuf::from("/tmp/wm-ts-nav")
}
}

View File

@@ -1,102 +0,0 @@
use anyhow::Result;
use ignore::WalkBuilder;
use rayon::prelude::*;
use std::collections::HashSet;
use std::path::Path;
use crate::db::{self, Db};
use crate::parser::{self, Lang};
pub struct IndexStats {
pub files_scanned: usize,
pub files_updated: usize,
pub files_removed: usize,
pub files_unchanged: usize,
}
/// Incrementally update the index for the given root directory.
/// Only re-parses files whose mtime has changed since last index.
pub fn update_index(db: &Db, root: &Path) -> Result<IndexStats> {
// Collect all supported files using `ignore` crate (respects .gitignore)
let files: Vec<_> = WalkBuilder::new(root)
.hidden(true)
.git_ignore(true)
.git_global(false)
.build()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false))
.filter(|e| Lang::from_path(e.path()).is_some())
.map(|e| e.into_path())
.collect();
let disk_paths: HashSet<String> = files
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
// Check which files need updating
let existing = db.all_indexed_paths()?;
// Remove files no longer on disk
let mut files_removed = 0;
db.begin()?;
for (path, _) in &existing {
if !disk_paths.contains(path) {
db.remove_file(path)?;
files_removed += 1;
}
}
db.commit()?;
// Figure out which files need re-parsing
let existing_map: std::collections::HashMap<&str, i64> = existing
.iter()
.map(|(p, m)| (p.as_str(), *m))
.collect();
let to_parse: Vec<_> = files
.iter()
.filter(|path| {
let path_str = path.to_string_lossy();
match existing_map.get(path_str.as_ref()) {
Some(&old_mtime) => {
// Check if mtime changed
db::mtime_secs(path).unwrap_or(0) != old_mtime
}
None => true, // New file
}
})
.collect();
let files_unchanged = files.len() - to_parse.len();
// Parse files in parallel
let results: Vec<_> = to_parse
.par_iter()
.filter_map(|path| {
let mtime = db::mtime_secs(path).ok()?;
match parser::parse_file(path) {
Ok(result) => Some((path.to_string_lossy().to_string(), mtime, result)),
Err(e) => {
eprintln!("warning: failed to parse {}: {e}", path.display());
None
}
}
})
.collect();
let files_updated = results.len();
// Write to db in a single transaction
db.begin()?;
for (path, mtime, result) in &results {
db.upsert_file(path, *mtime, &result.symbols, &result.refs)?;
}
db.commit()?;
Ok(IndexStats {
files_scanned: files.len(),
files_updated,
files_removed,
files_unchanged,
})
}

View File

@@ -1,270 +0,0 @@
mod db;
mod indexer;
mod parser;
use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "wm-ts-nav", about = "Tree-sitter code navigator for Windmill")]
struct Cli {
/// Root directory to index (defaults to current directory)
#[arg(short, long)]
root: Option<PathBuf>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Index/re-index the codebase
Index,
/// Show symbols in a file
Outline {
/// File path
file: PathBuf,
},
/// Search symbols by name pattern
Search {
/// Name pattern (supports SQL LIKE % wildcards)
pattern: String,
/// Filter by kind (function, struct, enum, trait, impl, etc.)
#[arg(short, long)]
kind: Option<String>,
/// Filter by parent (e.g. --parent ServiceName to find methods on that type)
#[arg(short, long)]
parent: Option<String>,
/// Max results
#[arg(short, long, default_value = "50")]
limit: usize,
},
/// Find symbol definition by exact name
Def {
/// Exact symbol name
name: String,
/// Filter by kind
#[arg(short, long)]
kind: Option<String>,
},
/// Find references to a symbol in code (skips comments and strings)
Refs {
/// Symbol name to find
name: String,
/// Max results
#[arg(short, long, default_value = "50")]
limit: usize,
/// Filter to files matching this substring
#[arg(short, long)]
file: Option<String>,
/// Show which function/symbol contains each reference
#[arg(short, long)]
caller: bool,
},
/// Extract and print a symbol's source code
Body {
/// Exact symbol name
name: String,
/// Filter by kind
#[arg(short, long)]
kind: Option<String>,
/// Filter to files matching this substring
#[arg(short, long)]
file: Option<String>,
},
/// Find what calls a symbol (who calls X?)
Callers {
/// Symbol name to find callers of
name: String,
/// Max results
#[arg(short, long, default_value = "50")]
limit: usize,
},
/// Find what a symbol calls (what does X call?)
Callees {
/// Exact symbol name
name: String,
/// Filter by kind
#[arg(short, long)]
kind: Option<String>,
/// Filter to files matching this substring
#[arg(short, long)]
file: Option<String>,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
let root = cli
.root
.unwrap_or_else(|| std::env::current_dir().expect("no cwd"));
let root = std::fs::canonicalize(&root)?;
let cache_dir = db::cache_dir_for(&root);
let db = db::Db::open(&cache_dir)?;
// Always update index incrementally before any query
let stats = indexer::update_index(&db, &root)?;
match cli.command {
Command::Index => {
println!(
"Indexed {} files: {} updated, {} unchanged, {} removed",
stats.files_scanned, stats.files_updated, stats.files_unchanged, stats.files_removed
);
}
Command::Outline { file } => {
let file = std::fs::canonicalize(&file)?;
let symbols = db.file_symbols(&file.to_string_lossy())?;
if symbols.is_empty() {
println!("No symbols found");
return Ok(());
}
for s in &symbols {
let parent = s
.parent
.as_deref()
.map(|p| format!(" [{p}]"))
.unwrap_or_default();
let sig = s
.signature
.as_deref()
.map(|s| format!(" {s}"))
.unwrap_or_default();
println!("L{}-{} {:12} {}{}{}", s.line, s.end_line, s.kind, s.name, parent, sig);
}
}
Command::Search {
pattern,
kind,
parent,
limit,
} => {
let results = db.search_symbols(&pattern, kind.as_deref(), parent.as_deref(), limit)?;
if results.is_empty() {
println!("No symbols matching '{pattern}'");
return Ok(());
}
for r in &results {
let sig = r
.signature
.as_deref()
.map(|s| format!(" {s}"))
.unwrap_or_default();
let parent_info = r
.parent
.as_deref()
.map(|p| format!(" [{p}]"))
.unwrap_or_default();
println!("{}:{} {:12} {}{}{}", r.path, r.line, r.kind, r.name, parent_info, sig);
}
}
Command::Def { name, kind } => {
let results = db.search_symbols(&name, kind.as_deref(), None, 100)?;
let exact: Vec<_> = results.iter().filter(|r| r.name == name).collect();
if exact.is_empty() {
println!("No definition found for '{name}'");
return Ok(());
}
for r in &exact {
let sig = r
.signature
.as_deref()
.map(|s| format!("\n {s}"))
.unwrap_or_default();
let parent = r
.parent
.as_deref()
.map(|p| format!(" [{p}]"))
.unwrap_or_default();
println!(
"{}:L{}-{} {} {}{}{}",
r.path, r.line, r.end_line, r.kind, r.name, parent, sig
);
}
}
Command::Refs {
name,
limit,
file,
caller,
} => {
let results = db.find_refs(&name, limit, file.as_deref(), caller)?;
if results.is_empty() {
println!("No references found for '{name}'");
return Ok(());
}
for r in &results {
let origin = r
.import_path
.as_deref()
.map(|p| format!(" ({p})"))
.unwrap_or_default();
let caller_info = r
.caller_name
.as_deref()
.map(|c| format!(" [{c}]"))
.unwrap_or_default();
println!("{}:{}{}{}", r.path, r.line, caller_info, origin);
}
}
Command::Body { name, kind, file } => {
let results = db.search_symbols(&name, kind.as_deref(), None, 100)?;
let mut exact: Vec<_> = results.into_iter().filter(|r| r.name == name).collect();
if let Some(ref f) = file {
exact.retain(|r| r.path.contains(f.as_str()));
}
if exact.is_empty() {
println!("No definition found for '{name}'");
return Ok(());
}
for (i, r) in exact.iter().enumerate() {
if i > 0 {
println!("\n---\n");
}
println!("{}:L{}-{}", r.path, r.line, r.end_line);
match std::fs::read_to_string(&r.path) {
Ok(contents) => {
let lines: Vec<&str> = contents.lines().collect();
let start = (r.line as usize).saturating_sub(1);
let end = (r.end_line as usize).min(lines.len());
for line in &lines[start..end] {
println!("{line}");
}
}
Err(e) => println!(" (error reading file: {e})"),
}
}
}
Command::Callers { name, limit } => {
let results = db.find_callers(&name, limit)?;
if results.is_empty() {
println!("No callers found for '{name}'");
return Ok(());
}
for r in &results {
println!(
"{}:L{}-{} {} {} → L{}",
r.path, r.caller_line, r.caller_end_line, r.caller_kind, r.caller_name, r.ref_line
);
}
}
Command::Callees { name, kind, file } => {
let results = db.find_callees(&name, kind.as_deref(), file.as_deref())?;
if results.is_empty() {
println!("No callees found for '{name}'");
return Ok(());
}
for r in &results {
let origin = r
.import_path
.as_deref()
.map(|p| format!(" ({p})"))
.unwrap_or_default();
println!("{}{}", r.name, origin);
}
}
}
Ok(())
}

View File

@@ -1,716 +0,0 @@
use anyhow::{Context, Result};
use std::path::Path;
use tree_sitter::{Node, Parser};
#[derive(Debug, Clone, serde::Serialize)]
pub struct Symbol {
pub name: String,
pub kind: String,
pub line: usize,
pub end_line: usize,
pub signature: Option<String>,
pub parent: Option<String>,
}
#[derive(Debug, Clone)]
pub struct IdentRef {
pub name: String,
pub line: usize,
/// Resolved import path if known (e.g. "windmill_common::error::Error")
pub import_path: Option<String>,
}
/// A `use` import with its scope
#[derive(Debug, Clone)]
pub struct ImportEntry {
/// The short name (e.g. "Error")
pub name: String,
/// Full path (e.g. "windmill_common::error::Error")
pub full_path: String,
/// Line where the use is declared
pub line: usize,
/// End of the scope this use lives in (file end for top-level, block end for scoped)
pub scope_end: usize,
}
pub struct ParseResult {
pub symbols: Vec<Symbol>,
pub refs: Vec<IdentRef>,
}
pub enum Lang {
Rust,
Typescript,
Tsx,
}
impl Lang {
pub fn from_path(path: &Path) -> Option<Self> {
match path.extension()?.to_str()? {
"rs" => Some(Self::Rust),
"tsx" | "jsx" => Some(Self::Tsx),
"ts" | "js" => Some(Self::Typescript),
"svelte" => Some(Self::Typescript), // we extract <script> block, which is pure TS
_ => None,
}
}
}
pub fn parse_file(path: &Path) -> Result<ParseResult> {
let lang = Lang::from_path(path).context("unsupported file type")?;
let source = std::fs::read_to_string(path)
.with_context(|| format!("reading {}", path.display()))?;
let code = match lang {
Lang::Typescript if path.extension().map(|e| e == "svelte").unwrap_or(false) => {
extract_svelte_script(&source)
}
_ => source,
};
let mut parser = Parser::new();
match lang {
Lang::Rust => {
parser.set_language(&tree_sitter_rust::LANGUAGE.into())?;
}
Lang::Typescript => {
parser.set_language(&tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into())?;
}
Lang::Tsx => {
parser.set_language(&tree_sitter_typescript::LANGUAGE_TSX.into())?;
}
}
let tree = parser
.parse(&code, None)
.context("tree-sitter parse failed")?;
let root = tree.root_node();
let mut symbols = Vec::new();
match lang {
Lang::Rust => extract_rust_symbols(root, &code, &mut symbols, None),
Lang::Typescript | Lang::Tsx => extract_ts_symbols(root, &code, &mut symbols, None),
}
let mut imports = Vec::new();
let file_end = code.lines().count();
match lang {
Lang::Rust => collect_rust_imports(root, &code, &mut imports, file_end),
Lang::Typescript | Lang::Tsx => collect_ts_imports(root, &code, &mut imports, file_end),
}
let mut refs = Vec::new();
collect_ident_refs(root, &code, &mut refs);
// Resolve import paths for refs
resolve_refs(&mut refs, &imports);
Ok(ParseResult { symbols, refs })
}
fn extract_svelte_script(source: &str) -> String {
// Preserve original line positions: script lines stay at their original line numbers,
// non-script lines become empty. Tree-sitter then reports correct line numbers.
let mut result = String::new();
let mut in_script = false;
for line in source.lines() {
let trimmed = line.trim_start();
if !in_script && trimmed.starts_with("<script") {
result.push('\n');
in_script = true;
} else if in_script && trimmed.starts_with("</script") {
result.push('\n');
in_script = false;
} else if in_script {
result.push_str(line);
result.push('\n');
} else {
result.push('\n');
}
}
result
}
fn extract_rust_symbols(node: Node, source: &str, symbols: &mut Vec<Symbol>, parent: Option<&str>) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"function_item" => {
if let Some(sym) = rust_function(child, source, parent) {
symbols.push(sym);
}
}
"struct_item" => {
if let Some(name) = child_by_field(child, "name", source) {
let sig = signature_up_to_body(child, source);
symbols.push(Symbol {
name: name.clone(),
kind: "struct".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(sig),
parent: parent.map(String::from),
});
}
}
"enum_item" => {
if let Some(name) = child_by_field(child, "name", source) {
let sig = signature_up_to_body(child, source);
symbols.push(Symbol {
name: name.clone(),
kind: "enum".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(sig),
parent: parent.map(String::from),
});
}
}
"trait_item" => {
if let Some(name) = child_by_field(child, "name", source) {
let sig = signature_up_to_body(child, source);
symbols.push(Symbol {
name: name.clone(),
kind: "trait".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(sig),
parent: parent.map(String::from),
});
// Recurse into trait body for methods
if let Some(body) = child.child_by_field_name("body") {
extract_rust_symbols(body, source, symbols, Some(&name));
}
}
}
"impl_item" => {
let impl_name = rust_impl_name(child, source);
let sig = signature_up_to_body(child, source);
let parent_name = impl_name.as_deref().unwrap_or("impl");
symbols.push(Symbol {
name: parent_name.to_string(),
kind: "impl".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(sig),
parent: parent.map(String::from),
});
// Recurse into impl body for methods
if let Some(body) = child.child_by_field_name("body") {
extract_rust_symbols(body, source, symbols, Some(parent_name));
}
}
"type_item" => {
if let Some(name) = child_by_field(child, "name", source) {
symbols.push(Symbol {
name,
kind: "type_alias".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(node_text(child, source).to_string()),
parent: parent.map(String::from),
});
}
}
"const_item" | "static_item" => {
if let Some(name) = child_by_field(child, "name", source) {
symbols.push(Symbol {
name,
kind: child.kind().trim_end_matches("_item").into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(signature_up_to_body(child, source)),
parent: parent.map(String::from),
});
}
}
"mod_item" => {
if let Some(name) = child_by_field(child, "name", source) {
symbols.push(Symbol {
name,
kind: "mod".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: None,
parent: parent.map(String::from),
});
}
}
"macro_definition" => {
if let Some(name) = child_by_field(child, "name", source) {
symbols.push(Symbol {
name,
kind: "macro".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: None,
parent: parent.map(String::from),
});
}
}
// Recurse into declaration_list (impl body, trait body)
"declaration_list" => {
extract_rust_symbols(child, source, symbols, parent);
}
_ => {}
}
}
}
fn rust_function(node: Node, source: &str, parent: Option<&str>) -> Option<Symbol> {
let name = child_by_field(node, "name", source)?;
let sig = rust_fn_signature(node, source);
Some(Symbol {
name,
kind: "function".into(),
line: node.start_position().row + 1,
end_line: node.end_position().row + 1,
signature: Some(sig),
parent: parent.map(String::from),
})
}
fn rust_fn_signature(node: Node, source: &str) -> String {
// Capture everything up to the block (the `{`)
let text = node_text(node, source);
if let Some(brace) = text.find('{') {
text[..brace].trim().to_string()
} else {
// No body (trait declaration)
text.lines().next().unwrap_or("").to_string()
}
}
fn rust_impl_name(node: Node, source: &str) -> Option<String> {
// impl [Trait for] Type
let mut cursor = node.walk();
let mut type_name = None;
let mut trait_name = None;
for child in node.children(&mut cursor) {
match child.kind() {
"type_identifier" | "scoped_type_identifier" | "generic_type" => {
if trait_name.is_none() && type_name.is_none() {
type_name = Some(node_text(child, source).to_string());
} else if type_name.is_some() {
// This is the type after "for"
trait_name = type_name.take();
type_name = Some(node_text(child, source).to_string());
}
}
_ => {}
}
}
match (trait_name, type_name) {
(Some(t), Some(ty)) => Some(format!("{t} for {ty}")),
(None, Some(ty)) => Some(ty),
_ => None,
}
}
fn extract_ts_symbols(node: Node, source: &str, symbols: &mut Vec<Symbol>, parent: Option<&str>) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"function_declaration" => {
if let Some(name) = child_by_field(child, "name", source) {
symbols.push(Symbol {
name,
kind: "function".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(signature_up_to_body(child, source)),
parent: parent.map(String::from),
});
}
}
"interface_declaration" => {
if let Some(name) = child_by_field(child, "name", source) {
symbols.push(Symbol {
name,
kind: "interface".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(signature_up_to_body(child, source)),
parent: parent.map(String::from),
});
}
}
"type_alias_declaration" => {
if let Some(name) = child_by_field(child, "name", source) {
symbols.push(Symbol {
name,
kind: "type_alias".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(node_text(child, source).to_string()),
parent: parent.map(String::from),
});
}
}
"enum_declaration" => {
if let Some(name) = child_by_field(child, "name", source) {
symbols.push(Symbol {
name,
kind: "enum".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(signature_up_to_body(child, source)),
parent: parent.map(String::from),
});
}
}
"class_declaration" => {
if let Some(name) = child_by_field(child, "name", source) {
symbols.push(Symbol {
name: name.clone(),
kind: "class".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(signature_up_to_body(child, source)),
parent: parent.map(String::from),
});
if let Some(body) = child.child_by_field_name("body") {
extract_ts_symbols(body, source, symbols, Some(&name));
}
}
}
"method_definition" | "abstract_method_definition"
| "abstract_method_signature" | "method_signature" => {
if let Some(name) = child_by_field(child, "name", source) {
symbols.push(Symbol {
name,
kind: "method".into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(signature_up_to_body(child, source)),
parent: parent.map(String::from),
});
}
}
"public_field_definition" => {
if let Some(name) = child_by_field(child, "name", source) {
let kind = if let Some(value) = child.child_by_field_name("value") {
match value.kind() {
"arrow_function" | "function_expression" | "function" => "method",
_ => "property",
}
} else {
"property"
};
symbols.push(Symbol {
name,
kind: kind.into(),
line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
signature: Some(signature_up_to_body(child, source)),
parent: parent.map(String::from),
});
}
}
"export_statement" | "program" => {
extract_ts_symbols(child, source, symbols, parent);
}
"lexical_declaration" | "variable_declaration" => {
// const/let/var foo = ...
ts_variable_decl(child, source, symbols, parent);
}
_ => {}
}
}
}
fn ts_variable_decl(
node: Node,
source: &str,
symbols: &mut Vec<Symbol>,
parent: Option<&str>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "variable_declarator" {
if let Some(name) = child_by_field(child, "name", source) {
// Check if the value is an arrow function or function expression
let kind = if let Some(value) = child.child_by_field_name("value") {
match value.kind() {
"arrow_function" | "function_expression" | "function" => "function",
_ => "const",
}
} else {
"const"
};
symbols.push(Symbol {
name,
kind: kind.into(),
line: node.start_position().row + 1,
end_line: node.end_position().row + 1,
signature: Some(signature_up_to_body(node, source)),
parent: parent.map(String::from),
});
}
}
}
}
fn child_by_field(node: Node, field: &str, source: &str) -> Option<String> {
Some(node_text(node.child_by_field_name(field)?, source).to_string())
}
fn node_text<'a>(node: Node, source: &'a str) -> &'a str {
&source[node.byte_range()]
}
fn signature_up_to_body(node: Node, source: &str) -> String {
let text = node_text(node, source);
// Find first `{` that starts a block body
if let Some(pos) = text.find('{') {
let sig = text[..pos].trim();
// Collapse whitespace
sig.split_whitespace().collect::<Vec<_>>().join(" ")
} else {
let first_line = text.lines().next().unwrap_or("");
first_line.trim().to_string()
}
}
/// Collect all identifier references in code, skipping comments and strings.
fn collect_ident_refs(node: Node, source: &str, refs: &mut Vec<IdentRef>) {
match node.kind() {
// Skip non-code nodes
"line_comment" | "block_comment" | "string_literal" | "raw_string_literal"
| "string" | "template_string" | "string_fragment" | "comment"
| "string_content" | "char_literal" => return,
_ => {}
}
if is_identifier_node(node.kind()) {
let text = node_text(node, source);
// Skip single-char identifiers and keywords
if text.len() > 1 {
refs.push(IdentRef {
name: text.to_string(),
line: node.start_position().row + 1,
import_path: None, // resolved later
});
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_ident_refs(child, source, refs);
}
}
fn is_identifier_node(kind: &str) -> bool {
matches!(
kind,
"identifier"
| "type_identifier"
| "field_identifier"
| "property_identifier"
| "shorthand_field_identifier"
)
}
/// Collect `use` declarations from Rust source.
/// Handles: `use foo::bar::Baz;`, `use foo::bar::{Baz, Qux};`, `use foo::bar as Alias;`
fn collect_rust_imports(node: Node, source: &str, imports: &mut Vec<ImportEntry>, file_end: usize) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"use_declaration" => {
let scope_end = find_scope_end(child, file_end);
let text = node_text(child, source);
parse_rust_use(text, child.start_position().row + 1, scope_end, imports);
}
// Recurse into blocks/functions to find scoped imports
"function_item" | "block" | "impl_item" | "mod_item" => {
let block_end = child.end_position().row + 1;
collect_rust_imports_scoped(child, source, imports, block_end);
}
_ => {}
}
}
}
fn collect_rust_imports_scoped(
node: Node,
source: &str,
imports: &mut Vec<ImportEntry>,
scope_end: usize,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"use_declaration" => {
let text = node_text(child, source);
parse_rust_use(text, child.start_position().row + 1, scope_end, imports);
}
"block" | "declaration_list" => {
collect_rust_imports_scoped(child, source, imports, scope_end);
}
_ => {}
}
}
}
/// Parse a Rust `use` statement text into ImportEntry items.
fn parse_rust_use(text: &str, line: usize, scope_end: usize, imports: &mut Vec<ImportEntry>) {
// Strip `use ` prefix and `;` suffix
let text = text.trim();
let text = text.strip_prefix("use ").unwrap_or(text);
let text = text.strip_suffix(';').unwrap_or(text).trim();
// Strip visibility (pub, pub(crate), etc.)
let text = if text.starts_with("pub") {
if let Some(rest) = text.strip_prefix("pub(") {
// pub(crate) use ..., pub(super) use ...
if let Some(after) = rest.find(')') {
rest[after + 1..].trim()
} else {
text
}
} else {
text.strip_prefix("pub ").unwrap_or(text).trim()
}
} else {
text
};
// Handle `use foo::bar::{A, B, C};`
if let Some(brace_start) = text.find('{') {
let prefix = &text[..brace_start];
let brace_end = text.rfind('}').unwrap_or(text.len());
let inner = &text[brace_start + 1..brace_end];
for item in inner.split(',') {
let item = item.trim();
if item.is_empty() {
continue;
}
// Handle `Foo as Bar`
let (orig, alias) = if let Some(as_pos) = item.find(" as ") {
(&item[..as_pos], &item[as_pos + 4..])
} else {
(item, item)
};
let alias = alias.trim();
let full = format!("{}{}", prefix, orig.trim());
if !alias.is_empty() && alias != "self" && alias != "*" {
imports.push(ImportEntry {
name: alias.to_string(),
full_path: full,
line,
scope_end,
});
}
}
} else {
// Simple: `use foo::bar::Baz;` or `use foo::bar::Baz as Alias;`
let (path, alias) = if let Some(as_pos) = text.find(" as ") {
(&text[..as_pos], &text[as_pos + 4..])
} else {
let name = text.rsplit("::").next().unwrap_or(text);
(text, name)
};
let alias = alias.trim();
if !alias.is_empty() && alias != "self" && alias != "*" {
imports.push(ImportEntry {
name: alias.to_string(),
full_path: path.trim().to_string(),
line,
scope_end,
});
}
}
}
/// Collect `import` declarations from TypeScript/Svelte.
fn collect_ts_imports(node: Node, source: &str, imports: &mut Vec<ImportEntry>, file_end: usize) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "import_statement" {
let text = node_text(child, source);
parse_ts_import(text, child.start_position().row + 1, file_end, imports);
}
}
}
/// Parse a TS `import { A, B } from 'module'` into ImportEntry items.
fn parse_ts_import(text: &str, line: usize, scope_end: usize, imports: &mut Vec<ImportEntry>) {
// Extract module path from `from '...'` or `from "..."`
let module = if let Some(from_pos) = text.find("from ") {
let rest = &text[from_pos + 5..];
rest.trim()
.trim_matches(|c| c == '\'' || c == '"' || c == ';')
.to_string()
} else {
return;
};
// Extract imported names from `{ A, B as C }`
if let Some(brace_start) = text.find('{') {
let brace_end = text.find('}').unwrap_or(text.len());
let inner = &text[brace_start + 1..brace_end];
for item in inner.split(',') {
let item = item.trim();
if item.is_empty() {
continue;
}
let (orig, alias) = if let Some(as_pos) = item.find(" as ") {
(&item[..as_pos], &item[as_pos + 4..])
} else {
(item, item)
};
let alias = alias.trim();
if !alias.is_empty() {
imports.push(ImportEntry {
name: alias.to_string(),
full_path: format!("{}.{}", module, orig.trim()),
line,
scope_end,
});
}
}
}
// Default import: `import Foo from '...'`
else {
let text_trimmed = text.trim().strip_prefix("import ").unwrap_or("");
if let Some(name_end) = text_trimmed.find(|c: char| c.is_whitespace()) {
let name = &text_trimmed[..name_end];
if !name.is_empty() && name != "type" && !name.starts_with('{') {
imports.push(ImportEntry {
name: name.to_string(),
full_path: format!("{}.default", module),
line,
scope_end,
});
}
}
}
}
/// Find the end line of the enclosing scope for a node.
fn find_scope_end(node: Node, file_end: usize) -> usize {
let mut parent = node.parent();
while let Some(p) = parent {
match p.kind() {
"block" | "declaration_list" | "function_item" => {
return p.end_position().row + 1;
}
_ => parent = p.parent(),
}
}
file_end
}
/// Resolve import paths for identifier refs.
fn resolve_refs(refs: &mut [IdentRef], imports: &[ImportEntry]) {
for r in refs.iter_mut() {
// Find the best matching import: same name, declared before the ref, ref within scope
let best = imports
.iter()
.filter(|imp| imp.name == r.name && imp.line <= r.line && r.line <= imp.scope_end)
.last(); // last = most recently declared (innermost scope)
if let Some(imp) = best {
r.import_path = Some(imp.full_path.clone());
}
}
}