Compare commits

..

21 Commits

Author SHA1 Message Date
Ruben Fiszel
82bfa9613c chore(main): release 1.657.2 (#8376)
* chore(main): release 1.657.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-15 05:05:49 +00:00
Ruben Fiszel
b1b9c984e3 make wmill init generated skills respect nonDottedPaths config (#8377)
* docs: add nonDottedPaths convention to CLAUDE.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(cli): update generated skills to use non-dotted path conventions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): make generated skills respect nonDottedPaths config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): inject nonDottedPaths placeholders in generate.py for skills.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: default system prompts to non-dotted path conventions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 05:05:31 +00:00
Travis Pew
eb03ebbb04 fix(cli): Fix nonDottedPaths handling in cli flow lock generation (#8375)
* fix(cli): preserve non-dotted flow lock filenames

* test(cli): add non-dotted path tests for generate-metadata and sync pull

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 04:36:04 +00:00
Ruben Fiszel
5296adeddf test: add powershell module detection and execution tests (#8373)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 04:11:43 +00:00
Ruben Fiszel
1a061892e9 chore(main): release 1.657.1 (#8372)
* chore(main): release 1.657.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-14 23:24:31 +00:00
Ruben Fiszel
daade374b3 restore flat module detection with file existence check (#8371)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 23:11:23 +00:00
Ruben Fiszel
3a268a9cf1 fix: powershell WindmillClient module loading on Windows workers (#8370)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 23:09:00 +00:00
Ruben Fiszel
b6da492d1b chore(main): release 1.657.0 (#8368)
* chore(main): release 1.657.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-14 04:52:06 +00:00
Ruben Fiszel
87215193ca system promps generate metadata 2026-03-14 04:51:54 +00:00
Alexander Petric
5df37fb0db feat: add datatable config support to CLI settings sync and backend export (#8024)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-14 04:44:11 +00:00
Ruben Fiszel
6fa2543647 chore(main): release 1.656.0 (#8346) 2026-03-13 22:32:57 +00:00
hugocasa
c431053a1e fix(frontend): prevent duplicate and reserved agent tool names (#8367)
* fix(frontend): prevent duplicate and reserved agent tool names

Extend tool name validation to detect duplicates within an agent step
and reserved names (like 'preprocessor', 'failure'). Show specific error
messages in the editor panel and red styling in the graph view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(frontend): remove duplicate banner for agent tool name errors

The inline per-tool error messages are sufficient — the panel-level
banner was redundant and showed a double error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 22:30:53 +00:00
Ruben Fiszel
a079dd500f i pkg 2026-03-13 22:21:28 +00:00
centdix
9d2c439e2a fix: resource drawer opening behind dialog in chat mode (#8328)
* fix: resource drawer opening behind dialog in chat mode

Integrate Modal into the Disposable z-index stacking system so drawers
opened from within a modal (e.g. "Add a new resource") correctly appear
above the dialog instead of behind it.

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

* fix: resource drawer opening behind dialog in chat mode

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

* refactor: simplify minZIndex tracking by removing unnecessary refcount

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

* fix: use map-based minZIndex tracking and conditional chat elevation

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

* fix: use plain object instead of Map for reactive minZIndex tracking

$state(new Map()) is not deeply reactive in Svelte 5 — only plain
objects and arrays are proxied. Replaced with Record<string, number>
so that property assignments properly trigger $derived updates.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:33:16 +00:00
hugocasa
fb12b31df0 fix(frontend): improve native mode alert message and fix workspaced tag detection (#8361)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:32:29 +00:00
Pyra
51933be3ca fix(cli): suppress verbose lock generation messages in generate-metadata (#8357)
* fix(cli): suppress verbose lock generation messages in generate-metadata

Pass noStaleMessage flag through to updateRawAppRunnables and
updateAppInlineScripts to suppress verbose "Generating lock for..."
messages when running generate-metadata command. Also fixes a stray
`}` character in a template literal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* feat(cli): show updated inline scripts in generate-metadata output

Display inline script names that were relocked when processing flows
and apps in the generate-metadata command output. For example:

  [4/5] app    u/admin/test__raw_app: a, b, c

This provides visibility into what work was done without verbose
per-script logging that clutters the output.

- Add AppLocksResult and FlowLocksResult types to track updated scripts
- Update internal functions to return lists of updated script names
- Display script names inline with progress in generate-metadata

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 20:32:08 +00:00
Pyra
404ae09d42 fix(cli): normalize path separators in generate-metadata folder filter for Windows (#8358)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 20:31:44 +00:00
Diego Imbert
e0e78442b7 Support T | T[] in debounce (#8340)
* Detect union types in TS

* display union type arguments

* Handle single values at accumulation time

* nit propagate otyp

* Python support

* npm package update
2026-03-13 20:31:25 +00:00
hugocasa
0d31c35f3e fix(frontend): filter webhook/email tokens by scope instead of label (#8363)
The backend already filters tokens by scope matching the script/flow
path. Remove the redundant client-side label prefix filter so that all
tokens with matching scopes are shown, not just those with a specific
label convention.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:31:06 +00:00
Pyra
060687b1fa fix(cli): exclude raw app backend files from script metadata generation (#8362)
Files inside .raw_app/backend/ were incorrectly being processed by
`script generate-metadata` and `generate-metadata --skip-flows --skip-apps`
because the filter only checked isFlowPath and isAppPath, but not isRawAppPath.

This caused backend runnables to be treated as standalone scripts, creating
incorrect .script.yaml files at wrong locations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 20:30:23 +00:00
HugoCasa
8301d86800 docs: rewrite Code Navigation section with MUST for outline/body and condensed limitations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:45:06 +01:00
67 changed files with 1874 additions and 383 deletions

View File

@@ -66,7 +66,7 @@ jobs:
with:
bun-version: 1.3.10
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version: "20"

View File

@@ -57,7 +57,7 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version: "20"
- uses: astral-sh/setup-uv@v6.2.1

View File

@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "20"
@@ -70,7 +70,7 @@ jobs:
cache-workspaces: backend
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "20"
@@ -124,7 +124,7 @@ jobs:
cache-workspaces: backend
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "20"

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"

View File

@@ -129,7 +129,7 @@ jobs:
with:
deno-version: v2.x
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version: "20"

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v3
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
@@ -21,7 +21,7 @@ jobs:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v3
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"

View File

@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: '20'

View File

@@ -1,5 +1,50 @@
# 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, use `wm-ts-nav` to explore (see Code Navigation below). Use `outline` to understand file structure, `body` to read specific symbols, `def`/`callers`/`callees` to trace code. Read `docs/` for domain context.
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.
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`
@@ -52,11 +52,9 @@ let { my_prop = $bindable(default_value) }: { my_prop?: string } = $props()
## Code Navigation
`wm-ts-nav` is an AST-aware code navigator. Use **Grep** for regex/pattern search. Use **wm-ts-nav** for structural queries — it skips comments/strings and understands symbol boundaries.
`wm-ts-nav` is an AST-aware code navigator. Use **wm-ts-nav** for structural queries — it skips comments/strings and understands symbol boundaries.
**Prefer wm-ts-nav over Read** to save context window:
- `outline <file>` instead of reading a full file — understand structure first, then `body` or Read for specifics
- `body "X"` instead of reading a full file to see one function/struct
**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
@@ -73,20 +71,14 @@ $NAV --root backend callers "X" # who calls X?
$NAV --root backend callees "X" # what does X call?
```
**Limitations** — syntax-level analysis, no type inference:
- Import paths are stored literally — `crate::X` and `super::X` pointing to the same type won't be linked
- Re-export chains (`pub use`) aren't followed — refs through different re-export paths won't connect
- Trait methods can't be resolved to their trait definition
- Nested `use` trees (`use foo::{bar::{A, B}, baz::C}`) aren't parsed correctly
- Glob imports (`use foo::*`) — refs won't show import origin
- Macro-generated symbols (e.g. `sqlx::FromRow`) — invisible to tree-sitter
- Single-char identifiers — intentionally filtered out of refs
**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
- `import * as ns` namespace imports — member accesses through `ns.X` aren't resolved
## Core Principles
- **Use `outline`/`body` to explore, then `Read` with offset/limit from the results before editing** — avoid reading full files
- **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

207
backend/Cargo.lock generated
View File

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

View File

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

View File

@@ -25,6 +25,50 @@ 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.
@@ -390,8 +434,19 @@ 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: metadata.descriptions.get(&arg_name).map(|d| d.to_string()),
otyp: union_otyp.or_else(|| {
metadata.descriptions.get(&arg_name).map(|d| d.to_string())
}),
name: arg_name,
typ,
has_default: has_default || default.is_some(),
@@ -441,6 +496,9 @@ 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)
}
@@ -1046,6 +1104,33 @@ 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,8 +363,12 @@ 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: None,
otyp,
name,
typ,
default: None,
@@ -374,13 +378,21 @@ fn parse_param(
}
// Pat::Object(ObjectPat { ... }) = todo!()
Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable) = match *left {
Pat::Ident(ident) => binding_ident_to_arg(symbol_table, type_resolver, &ident),
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)
}
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)
(name, typ, nullable, None)
}
_ => {
return Err(anyhow::anyhow!(
@@ -416,7 +428,7 @@ fn parse_param(
if typ == Typ::Unknown && dflt.is_some() {
typ = json_to_typ(dflt.as_ref().unwrap(), false);
}
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true, oidx: None })
Ok(Arg { otyp, 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);
@@ -961,6 +973,75 @@ 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,6 +646,55 @@ 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

@@ -1518,6 +1518,92 @@ 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.655.0
version: 1.657.2
title: Windmill API
contact:

View File

@@ -283,6 +283,8 @@ 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>,
@@ -323,6 +325,8 @@ 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>,
@@ -347,6 +351,7 @@ 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>,
@@ -955,6 +960,7 @@ pub(crate) async fn tarball_workspace(
mute_critical_alerts,
color,
operator_settings,
datatable,
slack_team_id,
slack_name,
slack_command_script
@@ -983,6 +989,7 @@ 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(),
@@ -1045,6 +1052,7 @@ 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,8 +3030,16 @@ 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(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}")));
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}")));
}
}
}
}
}

View File

@@ -143,5 +143,8 @@ 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#"^Import-Module\s+(?:-Name\s+)?"?([^\s"]+)"?(?:\s+-RequiredVersion\s+"?([^\s"]+)"?)?"#).unwrap();
static ref RE_POWERSHELL_IMPORTS: Regex = Regex::new(r#"^\s*Import-Module\s+(?:-Name\s+)?"?([^\s"]+)"?(?:\s+-RequiredVersion\s+"?([^\s"]+)"?)?"#).unwrap();
}
use crate::{
@@ -196,17 +196,41 @@ 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('.') {
versions.push(version);
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);
}
}
}
}
}
}
// If no version subdirectories found, treat as single version installation
// If no version subdirectories found, check if module files exist directly
// in the module directory (flat/single-version installation)
if versions.is_empty() {
versions.push("unknown".to_string());
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());
}
}
Ok(versions)
@@ -466,8 +490,8 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
let strict_termination_end = "\n\
} catch {\n\
Write-Output \"An error occurred:\n\"\
Write-Output $_
Write-Output \"An error occurred:\"\n\
Write-Output $_\n\
exit 1\n\
}\n";
@@ -672,3 +696,231 @@ $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.655.0";
export const VERSION = "v1.657.2";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

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

View File

@@ -29,7 +29,10 @@ 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, getFolderSuffix } from "../../utils/resource_folders.ts";
import {
extractNameFromFolder,
getNonDottedPaths,
} from "../../utils/resource_folders.ts";
const TOP_HASH = "__flow_hash";
async function generateFlowHash(
@@ -51,6 +54,14 @@ 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,
@@ -60,7 +71,7 @@ export async function generateFlowLockInternal(
},
justUpdateMetadataLock?: boolean,
noStaleMessage?: boolean
): Promise<string | void> {
): Promise<string | FlowLocksResult | void> {
if (folder.endsWith(SEP)) {
folder = folder.substring(0, folder.length - 1);
}
@@ -109,8 +120,9 @@ 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) {
@@ -148,7 +160,9 @@ export async function generateFlowLockInternal(
filteredDeps
);
const lockAssigner = newPathAssigner(opts.defaultTs ?? "bun");
const lockAssigner = newPathAssigner(opts.defaultTs ?? "bun", {
skipInlineScriptSuffix: getNonDottedPaths(),
});
const inlineScripts = extractInlineScriptsForFlows(
flowValue.value.modules,
{},
@@ -185,6 +199,13 @@ 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 } from "../flow/flow_metadata.ts";
import { generateAppLocksInternal, getAppFolders } from "../app/app_metadata.ts";
import { generateFlowLockInternal, FlowLocksResult } from "../flow/flow_metadata.ts";
import { generateAppLocksInternal, getAppFolders, AppLocksResult } from "../app/app_metadata.ts";
import {
elementsToMap,
FSFSElement,
ignoreF,
} from "../sync/sync.ts";
import { exts } from "../script/script.ts";
import { isFlowPath, isAppPath } from "../../utils/resource_folders.ts";
import { isFlowPath, isAppPath, isRawAppPath } from "../../utils/resource_folders.ts";
import { listSyncCodebases } from "../../utils/codebase.ts";
interface StaleItem {
@@ -82,7 +82,8 @@ async function generateMetadata(
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
ignore(p, isD) ||
isFlowPath(p) ||
isAppPath(p)
isAppPath(p) ||
isRawAppPath(p)
);
},
false,
@@ -192,12 +193,17 @@ async function generateMetadata(
// === Filter by folder if specified ===
let filteredItems = staleItems;
if (folder) {
// Strip trailing separator to match deprecated flow/app handler behavior
// (see generateFlowLockInternal line 64-66, generateAppLocksInternal line 109-110)
if (folder.endsWith(SEP)) {
// 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("/")) {
folder = folder.substring(0, folder.length - 1);
}
filteredItems = staleItems.filter((item) => item.folder === folder || item.folder.startsWith(folder + SEP));
// 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 + "/");
});
}
// === Show stale items and confirm ===
@@ -280,21 +286,23 @@ async function generateMetadata(
// Process flows
for (const item of flows) {
current++;
log.info(`${formatProgress(current)} flow ${colors.cyan(item.path)}`);
await generateFlowLockInternal(
const result = 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++;
log.info(`${formatProgress(current)} app ${colors.cyan(item.path)}`);
await generateAppLocksInternal(
const result = await generateAppLocksInternal(
item.folder,
item.isRawApp!, // rawApp
false, // dryRun
@@ -302,7 +310,11 @@ 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,6 +252,16 @@ 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
@@ -290,6 +300,20 @@ 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,6 +58,7 @@ import {
isFlowInlineScriptPath as isFlowInlineScriptPathInternal,
isFlowPath,
isAppPath,
isRawAppPath,
} from "../../utils/resource_folders.ts";
export interface ScriptFile {
@@ -1027,7 +1028,8 @@ export async function generateMetadata(
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
ignore(p, isD) ||
isFlowPath(p) ||
isAppPath(p)
isAppPath(p) ||
isRawAppPath(p)
);
},
false,

View File

@@ -53,6 +53,7 @@ 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;
@@ -100,6 +101,7 @@ 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;
@@ -192,6 +194,7 @@ 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,
@@ -382,6 +385,14 @@ 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\` and add a YAML file with the flow definition.
For rawscript modules, use \`!inline path/to/script.ts\` for the content key.
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}}
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_SUFFIX}} --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/
my_app{{RAW_APP_SUFFIX}}/
├── 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,6 +5072,23 @@ 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.655.0";
export const VERSION = "1.657.2";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";

View File

@@ -50,26 +50,25 @@ 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;
}
@@ -691,6 +690,11 @@ 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

@@ -0,0 +1,226 @@
/**
* 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): Promise<void> {
async function createRawAppOnDisk(appDir: string, includeBackend: boolean = false): Promise<void> {
await mkdir(appDir, { recursive: true });
await mkdir(path.join(appDir, "inline_scripts"), { recursive: true });
@@ -131,6 +131,16 @@ async function createRawAppOnDisk(appDir: string): Promise<void> {
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 () => {
@@ -153,7 +163,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);
await createRawAppOnDisk(appDir, true); // Include backend for metadata test
// =========================================================================
// STEP 1: Initial push - create raw app on backend
@@ -266,6 +276,47 @@ 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,6 +1768,66 @@ 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 { writeFile } from "node:fs/promises";
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import {
createLocalScript,
createLocalFlow,
@@ -19,7 +19,12 @@ import {
/**
* Helper to set up a workspace with wmill.yaml
*/
async function setupWorkspace(backend: any, tempDir: string, workspaceName: string) {
async function setupWorkspace(
backend: any,
tempDir: string,
workspaceName: string,
nonDottedPaths = false
) {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
@@ -29,11 +34,88 @@ async function setupWorkspace(backend: any, tempDir: string, workspaceName: stri
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
${nonDottedPaths ? "nonDottedPaths: true\n" : ""}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
// =============================================================================
@@ -156,6 +238,87 @@ 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

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.655.0",
"version": "1.657.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.655.0",
"version": "1.657.2",
"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.653.0",
"windmill-parser-wasm-py": "1.655.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.653.0",
"windmill-parser-wasm-ts": "1.655.0",
"windmill-parser-wasm-yaml": "1.593.0",
"windmill-sql-datatype-parser-wasm": "1.512.0",
"windmill-utils-internal": "^1.3.4",
@@ -837,6 +837,7 @@
"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": {
@@ -848,6 +849,7 @@
"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": {
@@ -858,6 +860,7 @@
"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": {
@@ -1347,6 +1350,7 @@
"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": {
@@ -1503,6 +1507,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1519,6 +1524,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1535,6 +1541,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1551,6 +1558,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1567,6 +1575,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1583,6 +1592,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1599,6 +1609,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1615,6 +1626,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1631,6 +1643,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1647,6 +1660,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1663,6 +1677,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1679,6 +1694,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1695,6 +1711,7 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1711,6 +1728,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1727,6 +1745,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2032,6 +2051,7 @@
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -6840,7 +6860,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -7339,6 +7359,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7359,6 +7380,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7379,6 +7401,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7399,6 +7422,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7419,6 +7443,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7439,6 +7464,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7459,6 +7485,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7479,6 +7506,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7499,6 +7527,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7519,6 +7548,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7539,6 +7569,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -12114,6 +12145,21 @@
}
}
},
"node_modules/svelte-check/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/svelte-eslint-parser": {
"version": "0.43.0",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
@@ -12844,7 +12890,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -13625,9 +13671,9 @@
"integrity": "sha512-u2qaMkupSdhJibxvkLh3r/y36IARvnYNTLXWvOKxcQ0G/BPUB4+yF5o/yf47vv9zUV5WZv4mrdsKDt/pZDYeDg=="
},
"node_modules/windmill-parser-wasm-py": {
"version": "1.653.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.653.0.tgz",
"integrity": "sha512-vMkSL3JpELpag7nmyGA8onhYNiAG3K1mkh2k4vwVHC3W5dUd12fSS9gsBco2FqPGUPnWv1gCaHwmjBrOBVGL1w=="
"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=="
},
"node_modules/windmill-parser-wasm-regex": {
"version": "1.653.0",
@@ -13645,9 +13691,9 @@
"integrity": "sha512-9yGLYZX2Hn9TdTqGY/5Fp50ftzgUsrfBkSK9vJkKJd5Amyg+yXLBGzd8pz6Org+4uxMenz/16wpsgijvo6uhhQ=="
},
"node_modules/windmill-parser-wasm-ts": {
"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=="
"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=="
},
"node_modules/windmill-parser-wasm-yaml": {
"version": "1.593.0",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.655.0",
"version": "1.657.2",
"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.653.0",
"windmill-parser-wasm-py": "1.655.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.653.0",
"windmill-parser-wasm-ts": "1.655.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,7 +291,12 @@
workers.some(([_, pings]) => pings.some((p) => p.native_mode === true)))
)
let nonNativeTags = $derived(
(nconfig?.worker_tags ?? []).filter((t) => !nativeTags.includes(t) && t !== 'flow')
(nconfig?.worker_tags ?? []).filter(
(t) =>
!nativeTags.some((nt) => t === nt || t.startsWith(`${nt}-`)) &&
t !== 'flow' &&
!t.startsWith('flow-')
)
)
let isAutoNativeMode = $derived(name === 'native')
let isNativeModeEnabled = $derived(nconfig?.native_mode === true || isAutoNativeMode)
@@ -569,8 +574,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(
', '
)}. Non-native jobs will be failed. This is fine if those custom tags are only used
for native language jobs.
)}. This is fine if jobs sent to those tags are native only, otherwise they will be
failed.
</Alert>
{/if}
{#if isNativeModeEnabled && nconfig?.worker_tags != undefined && !nconfig.worker_tags.includes(defaultTagPerWorkspace && workspaceTag ? `flow-${workspaceTag}` : 'flow')}

View File

@@ -1,5 +1,16 @@
<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">
@@ -11,6 +22,11 @@
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
@@ -21,13 +37,17 @@
id = (Math.random() + 1).toString(36).substring(10),
preventEscape = false,
initialOffset = 0,
minZIndex = 0,
children,
onOpen,
onClose
}: Props = $props()
let offset = $state(untrack(() => initialOffset))
let zIndex = $derived(zIndexes.disposables + offset)
// 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)
export function toggleDrawer() {
if (!open) {
@@ -44,6 +64,9 @@
}
openedDrawers.val.push(id)
offset = initialOffset + openedDrawers.val.length
if (minZIndex > 0) {
minZIndexEntries[id] = minZIndex
}
}
export function closeDrawer() {
@@ -51,6 +74,9 @@
offset = initialOffset
if (openedDrawers.val.includes(id)) {
openedDrawers.val = openedDrawers.val.filter((drawer) => drawer !== id)
if (minZIndex > 0) {
delete minZIndexEntries[id]
}
}
}
@@ -89,6 +115,9 @@
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,11 +2,14 @@
import { createBubbler, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } 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
@@ -28,12 +31,28 @@
cancelText = undefined,
kind = 'button',
settings,
children,
children: children_render,
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) {
@@ -58,70 +77,80 @@
<svelte:window onkeydowncapture={onKeyDown} />
{#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 -->
<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"
>
<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,
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
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'
? 'ease-out duration-300 opacity-100'
: 'ease-in duration-200 opacity-0'
)}
{style}
>
{#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>
<div class="mt-4 text-sm text-primary">
{@render children?.()}
<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}
>
{#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>
</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>
</div>
</div>
{/if}
{/if}
{/snippet}
</Disposable>

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

View File

@@ -30,11 +30,37 @@
fontClass?: string
} = $props()
// Get list of array-type arguments from schema
// 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.
let arrayArgs = $derived(
schema?.properties
? Object.entries(schema.properties)
.filter(([_, prop]) => prop.type === 'array')
.filter(([_, prop]) => prop.type === 'array' || isUnionWithArray(prop.originalType))
.map(([key, _]) => key)
: []
)
@@ -111,8 +137,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}>
@@ -131,8 +157,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} />
@@ -140,11 +166,16 @@
<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,6 +12,7 @@
action?: import('svelte').Snippet
children?: import('svelte').Snippet
isAgentTool?: boolean
siblingToolNames?: string[]
}
let {
@@ -23,7 +24,8 @@
header,
action,
children,
isAgentTool = false
isAgentTool = false,
siblingToolNames = undefined
}: Props = $props()
</script>
@@ -38,6 +40,7 @@
{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 { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import { getToolNameError } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import { DEFAULT_HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION } from '$lib/hub'
interface Props {
@@ -28,6 +28,7 @@
children?: import('svelte').Snippet
action?: import('svelte').Snippet
isAgentTool?: boolean
siblingToolNames?: string[]
}
let {
@@ -36,9 +37,14 @@
summary = $bindable(undefined),
children,
action,
isAgentTool = false
isAgentTool = false,
siblingToolNames = undefined
}: 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})
@@ -103,6 +109,7 @@
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" />
@@ -173,14 +180,16 @@
/>
</div>
{/if}
<input
bind:value={summary}
placeholder={isAgentTool ? 'Tool name' : 'Summary'}
class={twMerge(
'w-full grow',
isAgentTool && !validateToolName(summary ?? '') && '!border-red-400'
)}
/>
<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>
{:else if flowModuleValue.type === 'flow'}
<Badge color="indigo" capitalize>flow</Badge>
<input bind:value={summary} placeholder="Summary" class="w-full grow" />

View File

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

View File

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

View File

@@ -110,6 +110,7 @@
forceTestTab?: boolean
highlightArg?: string
isAgentTool?: boolean
siblingToolNames?: string[]
}
let {
@@ -125,7 +126,8 @@
savedModule = undefined,
forceTestTab = false,
highlightArg = undefined,
isAgentTool = false
isAgentTool = false,
siblingToolNames = undefined
}: Props = $props()
let workspaceScriptTag: string | undefined = $state(undefined)
@@ -237,7 +239,9 @@
}
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()
@@ -726,6 +730,7 @@
}}
bind:summary={flowModule.summary}
{isAgentTool}
{siblingToolNames}
>
{#snippet header()}
<FlowModuleHeader
@@ -1062,8 +1067,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,6 +305,7 @@
{enableAi}
{forceTestTab}
{highlightArg}
siblingToolNames={flowModule.value.tools.map((t) => t.summary ?? '')}
/>
{/if}
{/each}

View File

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

View File

@@ -1,10 +1,29 @@
<script module lang="ts">
export function validateToolName(name: string, type?: string) {
if (type === 'websearch') return true
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
return name.length > 0 ? undefined : 'Tool name must not be empty'
}
return /^[a-zA-Z0-9_]+$/.test(name)
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
}
export const AI_TOOL_BASE_OFFSET = 5
@@ -147,6 +166,7 @@
}
}
const siblingNames = tools.map((t) => t.name)
const toolNodes: (Node & AiToolN)[] = tools.map((tool, i) => {
let inputToolXGap = 12
let inputToolWidth = (ROW_WIDTH - inputToolXGap) / 2
@@ -160,6 +180,7 @@
data: {
tool: tool.name,
type: tool.type,
nameError: getToolNameError(tool.name, tool.type, siblingNames),
eventHandlers,
moduleId: tool.id,
insertable,
@@ -169,13 +190,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 *
@@ -287,7 +308,7 @@
const flowModuleState = $derived(data.flowModuleStates?.[data.moduleId])
let colorClasses = $derived(
getNodeColorClasses(
!validateToolName(data.tool, data.type) ? 'Failure' : flowModuleState?.type,
data.nameError ? 'Failure' : flowModuleState?.type,
selectionManager?.getSelectedId() === data.moduleId
)
)
@@ -324,12 +345,7 @@
<Wrench size={16} class="ml-1 shrink-0" />
{/if}
<span
class={twMerge(
'text-3xs truncate flex-1',
!validateToolName(data.tool, data.type) && 'text-red-400'
)}
>
<span class={twMerge('text-3xs truncate flex-1', data.nameError && '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,23 +10,21 @@
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 })
).filter((x) => x.label && x.label.startsWith(labelPrefix + '-'))
tokens = isFlow
? await FlowService.listTokensOfFlow({ workspace: $workspaceStore!, path })
: await ScriptService.listTokensOfScript({ workspace: $workspaceStore!, path })
if (labelPrefix == 'email') {
$triggersCount = { ...($triggersCount ?? {}), default_email_count: tokens?.length }
} else {
@@ -43,14 +41,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 {labelPrefix} specific tokens found</div>
<div class="text-xs text-secondary">No tokens with matching scopes 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,6 +384,11 @@ 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.655.0"
wmill = ">=1.657.2"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"

View File

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

View File

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

View File

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

View File

@@ -119,6 +119,23 @@ 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 YAML file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key.
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`).
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 YAML file with the flow definition.
For rawscript modules, use \`!inline path/to/script.ts\` for the content key.
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\`).
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,6 +1493,23 @@ 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,6 +124,23 @@ 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 YAML file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key.
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`).
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 YAML file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key.
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`).
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,6 +1094,20 @@ 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.655.0",
"version": "1.657.2",
"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.655.0",
"version": "1.657.2",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"sideEffects": false,

View File

@@ -1 +1 @@
1.655.0
1.657.2