feat: add powershell common parameters support (#8683)

* feat: add powershell common parameters support (-Verbose, -Debug, -ErrorAction, -WhatIf)

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

* feat: add powershell common params to script editor test panel

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

* fix: detect CmdletBinding from code instead of schema in script editor

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

* fix: ignore commented-out CmdletBinding in powershell detection

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

* fix: use preference variables for -Verbose/-Debug instead of CLI args

Verbose/Debug output goes to PowerShell stream 4/5 which isn't captured
by the 2>&1 redirect. Setting $VerbosePreference/$DebugPreference in the
wrapper scope propagates to child scripts and output flows through the
host to stderr, which Windmill captures as logs.

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

* fix: use *>&1 to capture all powershell streams including verbose/debug

The previous 2>&1 only captured error stream. Verbose (stream 4) and
debug (stream 5) output was silently lost. Using *>&1 redirects all
streams to success stream so they flow through Tee-Object into logs.

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

* fix: use targeted stream redirects (4>&1 5>&1 2>&1) instead of *>&1

*>&1 breaks $PSCmdlet.ShouldProcess() by redirecting internal streams.
Only redirect verbose (4), debug (5), and error (2) to success stream.

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

* fix: revert to 2>&1 redirect — stream 4/5 redirects break powershell

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

* fix: use 4>&1 5>&1 for verbose/debug capture, remove WhatIf support

Stream 4/5 redirects capture verbose/debug in the pipeline. WhatIf is
removed because $PSCmdlet.ShouldProcess() doesn't work when scripts
are invoked through Windmill's wrapper.

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

* fix: redirect verbose/debug to files to keep result pipeline clean

Verbose (4) and debug (5) streams are redirected to separate log files
during script execution, then output via Write-Host after the script
completes. This keeps them out of the Tee-Object pipeline (used for
result extraction) while still showing them in the job logs.

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

* fix: output verbose/debug to stderr via Console.Error for log capture

Write-Host goes to stdout which gets mixed with result output and
truncated by OSS log threshold. Using [Console]::Error.WriteLine()
writes to stderr which Windmill captures separately as logs, with
VERBOSE:/DEBUG: prefixes for clarity.

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

* fix: redirect script output to file only, send verbose/debug to stdout

The OSS log storage has a 9KB threshold. Previously, Tee-Object sent
the full JSON result to both stdout (logs) and the pipe file, eating
the log budget. Now script output goes only to the pipe file (> $pipe),
and only verbose/debug messages go to stdout for the log viewer.

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

* fix: preserve original Tee-Object behavior, append verbose/debug after

Keep the original wrapper behavior (Tee-Object to stdout + pipe file).
Only add 4>verbose.log 5>debug.log to capture those streams, and
output them at the end of logs.

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

* fix: inject preference vars into main.ps1 instead of CLI args

Passing -Verbose/-Debug as CLI args causes PowerShell module loading
to emit verbose noise. Instead, inject $VerbosePreference/$DebugPreference
inside main.ps1's try block so they only affect user code. Stream 4/5
are still redirected to files in the wrapper for log output.

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

* fix: restore common param toggles from previous job args on Run Again

Extract _wm_ps_* keys from loaded args and initialize the toggle
states in PowerShellCommonParams. Also strip them from main args
so they don't appear as unknown schema form inputs.

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

* feat: show active common param badges when section is collapsed

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

* fix: inject ErrorAction as preference variable instead of CLI arg

-ErrorAction as a CLI arg only affects the caller, not the script's
internal error handling. Setting $ErrorActionPreference inside main.ps1
correctly overrides the default 'Stop' behavior for the user's code.

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

* fix: ensure full backward compatibility with existing powershell scripts

- Only filter common param names when [CmdletBinding()] is present
  (without it, $Verbose etc. are regular user-defined parameters)
- Only add 4>verbose.log 5>debug.log and log output lines when common
  params are actually enabled — original wrapper is unchanged otherwise

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

* refactor: lighter styling for common params section

Replaced heavy Section component with a subtle inline chevron toggle
labeled "Common parameters". Smaller text, secondary color, indented
options. Badges still show when collapsed.

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

* fix: rename section to CmdletBinding parameters

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

* fix: add ..Default::default() to windmill-parser-r (new parser from main)

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

* fix: missing comma in graphql parser test + merge main

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

* fix: add missing commas before ..Default::default() in parser tests

Merge from main brought test constructors with formatting issues
from the original automated script (missing comma between last field
and ..Default::default()).

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

* fix: restore comment markers in nu parser test that script broke

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

* fix: address PR review — ignore commented CmdletBinding, clear stale params

1. Parser: strip comment lines before detecting [CmdletBinding()] to
   avoid false positives from commented-out attributes
2. RunForm: always assign psCommonParams (not just when non-empty) so
   stale settings from a previous run don't leak into later runs

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-04-03 09:03:22 -04:00
committed by GitHub
parent 7485a5db04
commit 0317d5891c
24 changed files with 558 additions and 123 deletions
+208 -27
View File
@@ -22,22 +22,82 @@ pub fn parse_bash_sig(code: &str) -> anyhow::Result<MainArgSignature> {
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
})
} else {
Err(anyhow!("Error parsing bash script".to_string()))
}
}
/// PowerShell common parameter names that are automatically added by [CmdletBinding()].
/// These should be filtered from the parsed signature since they are not user-defined.
const POWERSHELL_COMMON_PARAMS: &[&str] = &[
"verbose",
"debug",
"erroraction",
"errorvariable",
"informationaction",
"informationvariable",
"outvariable",
"outbuffer",
"pipelinevariable",
"warningaction",
"warningvariable",
"whatif",
"confirm",
"progressaction",
];
/// Detects whether the script uses [CmdletBinding()] and whether it declares SupportsShouldProcess.
fn detect_cmdlet_binding(code: &str) -> (bool, bool) {
let attr_region = match extract_powershell_param_block_with_attributes(code, true) {
Some((region, _)) => region,
None => return (false, false),
};
// Strip comment lines to avoid false positives from commented-out [CmdletBinding()]
let uncommented: String = attr_region
.lines()
.filter(|line| !line.trim_start().starts_with('#'))
.collect::<Vec<_>>()
.join("\n");
let lower = uncommented.to_lowercase();
let has_cmd_binding = lower.contains("[cmdletbinding");
let supports_should_process = has_cmd_binding
&& lower.contains("supportsshouldprocess")
&& !lower.contains("supportsshouldprocess=$false")
&& !lower.contains("supportsshouldprocess = $false");
(has_cmd_binding, supports_should_process)
}
pub fn parse_powershell_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_powershell_file(&code)?;
if let Some(x) = parsed {
let args = x;
if let Some(args) = parsed {
let (has_cmd_binding, supports_should_process) = detect_cmdlet_binding(code);
// Filter out common parameters only when CmdletBinding is present
// (without CmdletBinding, $Verbose etc. are regular user-defined parameters)
let args = if has_cmd_binding {
args.into_iter()
.filter(|arg| !POWERSHELL_COMMON_PARAMS.contains(&arg.name.to_lowercase().as_str()))
.collect()
} else {
args
};
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
auto_kind: None,
has_preprocessor: None,
has_cmd_binding: if has_cmd_binding { Some(true) } else { None },
supports_should_process: if supports_should_process {
Some(true)
} else {
None
},
..Default::default()
})
} else {
Err(anyhow!("Error parsing powershell script".to_string()))
@@ -96,7 +156,10 @@ fn parse_bash_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
/// This function uses the existing extract_powershell_param_block validation, which already
/// ensures that only comments, whitespace, and attributes appear before param. So we can
/// simply return everything from the beginning to the end of the param block.
pub fn extract_powershell_param_block_with_attributes(code: &str, include_attributes: bool) -> Option<(&str, &str)> {
pub fn extract_powershell_param_block_with_attributes(
code: &str,
include_attributes: bool,
) -> Option<(&str, &str)> {
// First, use the existing function to validate and find the param block
let param_block = extract_powershell_param_block(code, true)?;
@@ -450,11 +513,15 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result<Vec<Arg>> {
// Check if this is a Parameter attribute with Mandatory (case-insensitive)
let lower = bracket_content.to_lowercase();
if lower.starts_with("parameter(") || lower.starts_with("parameter ") {
if lower.starts_with("parameter(")
|| lower.starts_with("parameter ")
{
// Check for Mandatory (case-insensitive)
if lower.contains("mandatory") {
// Check if it's explicitly set to false
if !lower.contains("mandatory=$false") && !lower.contains("mandatory = $false") {
if !lower.contains("mandatory=$false")
&& !lower.contains("mandatory = $false")
{
is_mandatory = true;
}
}
@@ -471,7 +538,11 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result<Vec<Arg>> {
// Check if this looks like a type (simple word, possibly with [])
let is_type = !bracket_content.contains('(')
&& !bracket_content.contains('=')
&& (bracket_content.chars().next().unwrap_or(' ').is_alphabetic()
&& (bracket_content
.chars()
.next()
.unwrap_or(' ')
.is_alphabetic()
|| bracket_content.starts_with('['));
if is_type && !found_dollar {
@@ -529,7 +600,9 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result<Vec<Arg>> {
while let Some((i, ch)) = chars.peek().copied() {
if in_string {
if ch == string_char && content.chars().nth(i.saturating_sub(1)) != Some('`') {
if ch == string_char
&& content.chars().nth(i.saturating_sub(1)) != Some('`')
{
in_string = false;
default_end = i + 1;
chars.next();
@@ -544,7 +617,9 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result<Vec<Arg>> {
chars.next();
} else if ch == ',' {
break;
} else if ch.is_whitespace() && chars.clone().skip(1).next().map(|(_, c)| c) == Some(',') {
} else if ch.is_whitespace()
&& chars.clone().skip(1).next().map(|(_, c)| c) == Some(',')
{
break;
} else {
default_end = i + 1;
@@ -552,12 +627,19 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result<Vec<Arg>> {
}
}
default_value = Some(content[default_start..default_end].trim().to_string());
default_value =
Some(content[default_start..default_end].trim().to_string());
}
',' => {
// End of parameter, finalize it
if let Some(name) = var_name.take() {
args.push(finalize_parameter(name, type_annotation.take(), default_value.take(), is_mandatory, validate_set.take())?);
args.push(finalize_parameter(
name,
type_annotation.take(),
default_value.take(),
is_mandatory,
validate_set.take(),
)?);
}
// Reset for next parameter
@@ -576,7 +658,13 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result<Vec<Arg>> {
// Finalize last parameter
if let Some(name) = var_name {
args.push(finalize_parameter(name, type_annotation, default_value, is_mandatory, validate_set)?);
args.push(finalize_parameter(
name,
type_annotation,
default_value,
is_mandatory,
validate_set,
)?);
}
Ok(args)
@@ -722,7 +810,8 @@ non_required="${5:-}"
}
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -813,14 +902,19 @@ non_required="${5:-}"
Arg {
otyp: Some("string".to_string()), // [string] (last type bracket with Mandatory and ValidateSet)
name: "Message".to_string(),
typ: Typ::Str(Some(vec!["Green".to_string(), "Blue".to_string(), "Red".to_string()])), // ValidateSet enum
typ: Typ::Str(Some(vec![
"Green".to_string(),
"Blue".to_string(),
"Red".to_string()
])), // ValidateSet enum
default: None,
has_default: false, // Required (Mandatory attribute)
oidx: None
}
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
Ok(())
@@ -970,19 +1064,13 @@ non_required="${5:-}"
// Valid: CmdletBinding with comments
assert_eq!(
extract_powershell_param_block(
"# My function\n[CmdletBinding()]\nparam($Name)",
false
),
extract_powershell_param_block("# My function\n[CmdletBinding()]\nparam($Name)", false),
Some("$Name")
);
// Valid: CmdletBinding with whitespace variations
assert_eq!(
extract_powershell_param_block(
"[CmdletBinding()] \n param($Name)",
false
),
extract_powershell_param_block("[CmdletBinding()] \n param($Name)", false),
Some("$Name")
);
@@ -1204,17 +1292,32 @@ param(
// Test with CmdletBinding with parameters
let code3 = "[CmdletBinding(DefaultParameterSetName='ByName')]\nparam($Name, $Id)";
let result3 = extract_powershell_param_block_with_attributes(code3, true);
assert_eq!(result3, Some(("[CmdletBinding(DefaultParameterSetName='ByName')]\nparam($Name, $Id)", "")));
assert_eq!(
result3,
Some((
"[CmdletBinding(DefaultParameterSetName='ByName')]\nparam($Name, $Id)",
""
))
);
// Test with multiple attributes
let code4 = "[CmdletBinding()]\n[OutputType([string])]\nparam($Value)";
let result4 = extract_powershell_param_block_with_attributes(code4, true);
assert_eq!(result4, Some(("[CmdletBinding()]\n[OutputType([string])]\nparam($Value)", "")));
assert_eq!(
result4,
Some((
"[CmdletBinding()]\n[OutputType([string])]\nparam($Value)",
""
))
);
// Test with comment before attributes
let code5 = "# My function\n[CmdletBinding()]\nparam($Name)";
let result5 = extract_powershell_param_block_with_attributes(code5, true);
assert_eq!(result5, Some(("# My function\n[CmdletBinding()]\nparam($Name)", "")));
assert_eq!(
result5,
Some(("# My function\n[CmdletBinding()]\nparam($Name)", ""))
);
// Test with include_attributes = false (should only get param block, not attributes)
let code6 = "[CmdletBinding()]\nparam($Name)";
@@ -1224,7 +1327,10 @@ param(
// Test with code after param
let code7 = "[CmdletBinding()]\nparam($Name)\nWrite-Host 'Hello'";
let result7 = extract_powershell_param_block_with_attributes(code7, true);
assert_eq!(result7, Some(("[CmdletBinding()]\nparam($Name)", "\nWrite-Host 'Hello'")));
assert_eq!(
result7,
Some(("[CmdletBinding()]\nparam($Name)", "\nWrite-Host 'Hello'"))
);
// Test with code after param (without attributes)
let code8 = "[CmdletBinding()]\nparam($Name)\nWrite-Host 'Hello'";
@@ -1392,10 +1498,85 @@ param(
}
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
Ok(())
}
#[test]
fn test_detect_cmdlet_binding() {
// Basic CmdletBinding
let (has_cb, has_ssp) = detect_cmdlet_binding("[CmdletBinding()]\nparam($Name)");
assert!(has_cb);
assert!(!has_ssp);
// CmdletBinding with SupportsShouldProcess
let (has_cb, has_ssp) =
detect_cmdlet_binding("[CmdletBinding(SupportsShouldProcess=$true)]\nparam($Path)");
assert!(has_cb);
assert!(has_ssp);
// CmdletBinding with SupportsShouldProcess=false
let (has_cb, has_ssp) =
detect_cmdlet_binding("[CmdletBinding(SupportsShouldProcess=$false)]\nparam($Path)");
assert!(has_cb);
assert!(!has_ssp);
// No CmdletBinding
let (has_cb, has_ssp) = detect_cmdlet_binding("param($Name)");
assert!(!has_cb);
assert!(!has_ssp);
// Case insensitive
let (has_cb, has_ssp) =
detect_cmdlet_binding("[cmdletbinding(supportsshouldprocess=$true)]\nparam($X)");
assert!(has_cb);
assert!(has_ssp);
// Commented out CmdletBinding should NOT be detected
let (has_cb, has_ssp) =
detect_cmdlet_binding("# [CmdletBinding(SupportsShouldProcess=$true)]\nparam($Path)");
assert!(!has_cb);
assert!(!has_ssp);
}
#[test]
fn test_powershell_common_param_filtering() -> anyhow::Result<()> {
// Common parameters declared in param() should be filtered out
let code = r#"[CmdletBinding()]
param(
[string]$Name,
[switch]$Verbose,
[string]$ErrorAction,
[int]$Age
)"#;
let sig = parse_powershell_sig(code)?;
assert_eq!(sig.args.len(), 2);
assert_eq!(sig.args[0].name, "Name");
assert_eq!(sig.args[1].name, "Age");
assert_eq!(sig.has_cmd_binding, Some(true));
assert_eq!(sig.supports_should_process, None);
Ok(())
}
#[test]
fn test_powershell_sig_cmdlet_binding_metadata() -> anyhow::Result<()> {
// Script without CmdletBinding
let code = "param([string]$Name)";
let sig = parse_powershell_sig(code)?;
assert_eq!(sig.has_cmd_binding, None);
assert_eq!(sig.supports_should_process, None);
// Script with CmdletBinding + SupportsShouldProcess
let code = "[CmdletBinding(SupportsShouldProcess=$true)]\nparam([string]$Path)";
let sig = parse_powershell_sig(code)?;
assert_eq!(sig.has_cmd_binding, Some(true));
assert_eq!(sig.supports_should_process, Some(true));
assert_eq!(sig.args.len(), 1);
assert_eq!(sig.args[0].name, "Path");
Ok(())
}
}
@@ -89,6 +89,7 @@ pub fn parse_csharp_sig_meta(code: &str) -> anyhow::Result<CsharpMainSigMeta> {
args,
has_preprocessor: None,
auto_kind,
..Default::default()
};
Ok(CsharpMainSigMeta { is_async, returns_void, class_name, main_sig, is_public })
@@ -43,6 +43,7 @@ pub fn parse_go_sig(code: &str) -> anyhow::Result<MainArgSignature> {
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
})
} else {
Ok(MainArgSignature {
@@ -51,6 +52,7 @@ pub fn parse_go_sig(code: &str) -> anyhow::Result<MainArgSignature> {
args: vec![],
auto_kind: Some("lib".to_string()),
has_preprocessor: None,
..Default::default()
})
}
}
@@ -244,7 +246,8 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
},
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -21,6 +21,7 @@ pub fn parse_graphql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
@@ -126,7 +127,8 @@ query($i: Int, $arr: [String]!, $wahoo: String = "wahoo") {
}
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -81,6 +81,7 @@ pub fn parse_java_sig_meta(code: &str) -> anyhow::Result<JavaMainSigMeta> {
args,
has_preprocessor: None,
auto_kind,
..Default::default()
};
Ok(JavaMainSigMeta { returns_void, class_name, main_sig, is_public })
@@ -56,6 +56,7 @@ mod test {
],
auto_kind: None,
has_preprocessor: None,
..Default::default()
},
sig
);
@@ -83,6 +84,7 @@ mod test {
},],
auto_kind: None,
has_preprocessor: None,
..Default::default()
},
sig
);
@@ -120,6 +122,7 @@ mod test {
],
auto_kind: None,
has_preprocessor: None,
..Default::default()
},
sig
);
@@ -232,6 +235,7 @@ mod test {
],
auto_kind: None,
has_preprocessor: None,
..Default::default()
},
sig
);
@@ -279,6 +283,7 @@ mod test {
],
auto_kind: None,
has_preprocessor: None,
..Default::default()
},
sig
);
@@ -343,6 +348,7 @@ mod test {
// },],
// auto_kind: None,
// has_preprocessor: None,
// ..Default::default()
// },
// sig
// );
@@ -373,6 +379,7 @@ mod test {
},],
auto_kind: None,
has_preprocessor: None,
..Default::default()
},
sig
);
@@ -420,6 +427,7 @@ mod test {
],
auto_kind: None,
has_preprocessor: None,
..Default::default()
},
sig
);
@@ -448,6 +456,7 @@ mod test {
},],
auto_kind: None,
has_preprocessor: None,
..Default::default()
},
sig
);
@@ -480,6 +489,7 @@ mod test {
// },],
// auto_kind: None,
// has_preprocessor: None,
// ..Default::default()
// },
// sig
// );
@@ -542,6 +552,7 @@ mod test {
],
auto_kind: None,
has_preprocessor: None,
..Default::default()
},
sig
);
@@ -635,6 +646,7 @@ mod test {
// ],
// auto_kind: None,
// has_preprocessor: None,
// ..Default::default()
// },
// sig
// );
@@ -101,6 +101,7 @@ pub fn parse_php_signature(
args,
auto_kind: None,
has_preprocessor,
..Default::default()
})
} else {
Ok(MainArgSignature {
@@ -109,6 +110,7 @@ pub fn parse_php_signature(
args: vec![],
auto_kind: Some("lib".to_string()),
has_preprocessor,
..Default::default()
})
}
}
@@ -180,7 +182,8 @@ function main(string $input1 = \"hey\", bool $input2 = false, int $input3 = 3, f
}
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
+21 -9
View File
@@ -366,6 +366,7 @@ pub fn parse_python_signature(
Some("lib".to_string())
},
has_preprocessor: Some(has_preprocessor),
..Default::default()
});
}
@@ -477,6 +478,7 @@ pub fn parse_python_signature(
.collect(),
auto_kind: None,
has_preprocessor: Some(has_preprocessor),
..Default::default()
})
} else {
Ok(MainArgSignature {
@@ -489,6 +491,7 @@ pub fn parse_python_signature(
None
},
has_preprocessor: Some(has_preprocessor),
..Default::default()
})
}
}
@@ -755,7 +758,8 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
},
],
auto_kind: None,
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -820,7 +824,8 @@ def main(test1: str,
}
],
auto_kind: None,
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -880,7 +885,8 @@ def main(test1: str,
}
],
auto_kind: None,
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -924,7 +930,8 @@ def main(test1: Literal["foo", "bar"], test2: List[Literal["foo", "bar"]]): retu
}
],
auto_kind: None,
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -955,7 +962,8 @@ def main(test1: DynSelect_foo): return
oidx: None
}],
auto_kind: None,
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -979,7 +987,8 @@ def hello(): return
star_kwargs: false,
args: vec![],
auto_kind: Some("lib".to_string()),
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -1007,7 +1016,8 @@ def main(): return
star_kwargs: false,
args: vec![],
auto_kind: None,
has_preprocessor: Some(true)
has_preprocessor: Some(true),
..Default::default()
}
);
@@ -1072,7 +1082,8 @@ def main(a: list, e: List[int], b: list = [1,2,3,4], c = [1,2,3,4], d = ["a", "b
}
],
auto_kind: None,
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -1121,7 +1132,8 @@ def main(a: str, b: Optional[str], c: str | None): return
},
],
auto_kind: None,
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -30,6 +30,7 @@ pub fn parse_r_sig_meta(code: &str) -> anyhow::Result<MainArgSignature> {
args: args.unwrap_or_default(),
has_preprocessor: None,
auto_kind: None,
..Default::default()
};
Ok(main_sig)
@@ -41,6 +41,7 @@ pub fn parse_ruby_sig_meta(code: &str) -> anyhow::Result<MainArgSignature> {
args: args.unwrap_or_default(),
has_preprocessor: None,
auto_kind,
..Default::default()
};
Ok(main_sig)
@@ -30,6 +30,7 @@ pub fn parse_rust_signature(code: &str) -> anyhow::Result<MainArgSignature> {
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
})
} else {
Ok(MainArgSignature {
@@ -38,6 +39,7 @@ pub fn parse_rust_signature(code: &str) -> anyhow::Result<MainArgSignature> {
args: vec![],
auto_kind: Some("lib".to_string()),
has_preprocessor: None,
..Default::default()
})
}
}
+35 -14
View File
@@ -30,6 +30,7 @@ pub fn parse_mysql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
@@ -46,6 +47,7 @@ pub fn parse_oracledb_sig(code: &str) -> anyhow::Result<MainArgSignature> {
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
@@ -67,6 +69,7 @@ pub fn parse_pgsql_sig_with_typed_schema(code: &str) -> anyhow::Result<(MainArgS
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
},
typed_schema,
))
@@ -85,6 +88,7 @@ pub fn parse_bigquery_sig(code: &str) -> anyhow::Result<MainArgSignature> {
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
@@ -100,6 +104,7 @@ pub fn parse_duckdb_sig(code: &str) -> anyhow::Result<MainArgSignature> {
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
@@ -116,6 +121,7 @@ pub fn parse_snowflake_sig(code: &str) -> anyhow::Result<MainArgSignature> {
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
@@ -132,6 +138,7 @@ pub fn parse_mssql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
@@ -944,7 +951,8 @@ SELECT * FROM table WHERE token=$1::TEXT AND image=$2::BIGINT
},
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -993,7 +1001,8 @@ SELECT $2::TEXT;
},
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1120,7 +1129,8 @@ SELECT ?, ?;
},
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1168,7 +1178,8 @@ SELECT :param2;
},
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1208,7 +1219,8 @@ SELECT @token;
},
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1256,7 +1268,8 @@ SELECT ?;
}
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1304,7 +1317,8 @@ SELECT @P2;
},
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1353,7 +1367,8 @@ SELECT * FROM table_name WHERE thing = :name4;
},
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1391,7 +1406,8 @@ SELECT * FROM users WHERE id = $1 AND email = $2::text;
},
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1429,7 +1445,8 @@ SELECT * FROM users LIMIT $1 OFFSET $2;
},
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1479,7 +1496,8 @@ WHERE id = $1
},
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1506,7 +1524,8 @@ SELECT * FROM users WHERE id = ANY($1);
oidx: Some(1),
},],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1535,7 +1554,8 @@ SELECT $1::integer;
oidx: Some(1),
},],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -1567,7 +1587,8 @@ SELECT x
oidx: None,
},],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -462,6 +462,7 @@ pub fn parse_deno_signature(
},
auto_kind,
has_preprocessor: Some(has_preprocessor),
..Default::default()
};
Ok(r)
}
@@ -47,6 +47,7 @@ mod tests {
args: vec![],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -105,6 +106,7 @@ mod tests {
],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -154,6 +156,7 @@ mod tests {
],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -203,6 +206,7 @@ mod tests {
],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -234,6 +238,7 @@ mod tests {
},],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -265,6 +270,7 @@ mod tests {
},],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -305,6 +311,7 @@ mod tests {
}],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -343,6 +350,7 @@ mod tests {
}],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -401,6 +409,7 @@ mod tests {
}],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -459,6 +468,7 @@ mod tests {
],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -508,6 +518,7 @@ mod tests {
],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -557,6 +568,7 @@ mod tests {
args: vec![],
auto_kind: Some("lib".to_string()),
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -584,6 +596,7 @@ mod tests {
}],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -613,6 +626,7 @@ mod tests {
}],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -642,6 +656,7 @@ mod tests {
}],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -691,6 +706,7 @@ mod tests {
],
auto_kind: None,
has_preprocessor: Some(false),
..Default::default()
}
);
}
@@ -731,6 +747,7 @@ mod tests {
}],
auto_kind: None,
has_preprocessor: Some(true),
..Default::default()
}
);
}
@@ -761,6 +778,7 @@ mod tests {
}],
auto_kind: None,
has_preprocessor: Some(true),
..Default::default()
}
);
}
@@ -141,7 +141,8 @@ export function main(test1?: string, test2: string = \"burkina\",
}
],
auto_kind: None,
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -220,7 +221,8 @@ export function main(test2 = \"burkina\",
}
],
auto_kind: None,
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -271,7 +273,8 @@ export function main(foo: FooBar, {a, b}: FooBar, {c, d}: FooBar = {a: \"foo\",
}
],
auto_kind: None,
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -303,7 +306,8 @@ export function main(foo: (\"foo\" | \"bar\")[]) {
oidx: None
}],
auto_kind: None,
has_preprocessor: Some(false)
has_preprocessor: Some(false),
..Default::default()
}
);
@@ -447,7 +451,8 @@ Write-Output 'Testing...'
}
],
auto_kind: None,
has_preprocessor: None
has_preprocessor: None,
..Default::default()
}
);
@@ -27,6 +27,7 @@ pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result<MainArgSignature
args: vec![],
auto_kind: None,
has_preprocessor: None,
..Default::default()
});
}
@@ -94,6 +95,7 @@ pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result<MainArgSignature
args,
auto_kind: None,
has_preprocessor: None,
..Default::default()
})
}
@@ -38,6 +38,10 @@ pub struct MainArgSignature {
pub args: Vec<Arg>,
pub auto_kind: Option<String>,
pub has_preprocessor: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_cmd_binding: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_should_process: Option<bool>,
}
#[derive(Serialize, Clone, Debug, PartialEq)]
+59 -11
View File
@@ -337,7 +337,7 @@ pub async fn handle_powershell_job(
envs: HashMap<String, String>,
occupancy_metrics: &mut OccupancyMetrics,
) -> Result<Box<RawValue>, Error> {
let pwsh_args = {
let (pwsh_args, ps_preferences) = {
let args = build_args_map(job, client, &db).await?.map(Json);
let job_args = if args.is_some() {
args.as_ref()
@@ -347,7 +347,7 @@ pub async fn handle_powershell_job(
let parsed_sig = windmill_parser_bash::parse_powershell_sig(&content)?;
parsed_sig
let user_args = parsed_sig
.args
.iter()
.filter_map(|arg| {
@@ -384,7 +384,34 @@ pub async fn handle_powershell_job(
}
})
.collect::<Vec<_>>()
.join(" ")
.join(" ");
// Extract PowerShell common parameters (_wm_ps_* keys)
// All common params are injected as preference variables into main.ps1
// (not CLI args) so they only affect user code, not module loading.
let mut preference_lines: Vec<String> = Vec::new();
if let Some(args_map) = job_args {
if let Some(v) = args_map.get("_wm_ps_verbose") {
if serde_json::from_str::<bool>(v.get()).unwrap_or(false) {
preference_lines.push("$VerbosePreference = 'Continue'".to_string());
}
}
if let Some(v) = args_map.get("_wm_ps_debug") {
if serde_json::from_str::<bool>(v.get()).unwrap_or(false) {
preference_lines.push("$DebugPreference = 'Continue'".to_string());
}
}
if let Some(v) = args_map.get("_wm_ps_error_action") {
if let Ok(action) = serde_json::from_str::<String>(v.get()) {
if matches!(action.as_str(), "Stop" | "Continue" | "SilentlyContinue") {
preference_lines.push(format!("$ErrorActionPreference = '{action}'"));
}
}
}
}
let ps_preferences = preference_lines.join("\n");
(user_args, ps_preferences)
};
// Resolve modules from workspace dependencies and/or script imports
@@ -570,12 +597,22 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
}\n";
// make sure param() with its attributes is first
let preferences_section = if ps_preferences.is_empty() {
String::new()
} else {
format!("{}\n", ps_preferences)
};
let content: String = if let Some((param_block, remaining_code)) =
windmill_parser_bash::extract_powershell_param_block_with_attributes(&content, true)
{
format!(
"{}\n{}\n{}\n{}\n{}",
param_block, profile, strict_termination_start, remaining_code, strict_termination_end
"{}\n{}\n{}\n{}{}\n{}",
param_block,
profile,
strict_termination_start,
preferences_section,
remaining_code,
strict_termination_end
)
} else {
format!("{}\n{}", profile, content)
@@ -583,18 +620,29 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
write_file(job_dir, "main.ps1", content.as_str())?;
write_file(
job_dir,
"wrapper.ps1",
&format!(
let has_common_params = !ps_preferences.is_empty();
let wrapper_content = if has_common_params {
format!(
"$ErrorActionPreference = 'Stop'\n\
$pipe = New-TemporaryFile\n\
./main.ps1 {pwsh_args} 4>verbose.log 5>debug.log 2>&1 | Tee-Object -FilePath $pipe\n\
Get-Content -Path $pipe | Select-Object -Last 1 | Set-Content -Path './result2.out'\n\
if (Test-Path verbose.log) {{ Get-Content verbose.log | ForEach-Object {{ Write-Output \"VERBOSE: $_\" }} }}\n\
if (Test-Path debug.log) {{ Get-Content debug.log | ForEach-Object {{ Write-Output \"DEBUG: $_\" }} }}\n\
Remove-Item $pipe\n\
exit $LASTEXITCODE\n"
)
} else {
format!(
"$ErrorActionPreference = 'Stop'\n\
$pipe = New-TemporaryFile\n\
./main.ps1 {pwsh_args} 2>&1 | Tee-Object -FilePath $pipe\n\
Get-Content -Path $pipe | Select-Object -Last 1 | Set-Content -Path './result2.out'\n\
Remove-Item $pipe\n\
exit $LASTEXITCODE\n"
),
)?;
)
};
write_file(job_dir, "wrapper.ps1", &wrapper_content)?;
let mut reserved_variables =
get_reserved_variables(job, &client.token, db, parent_runnable_path).await?;
+6 -52
View File
@@ -84,7 +84,7 @@
"windmill-parser-wasm-php": "1.647.1",
"windmill-parser-wasm-py": "1.657.2",
"windmill-parser-wasm-r": "1.668.1",
"windmill-parser-wasm-regex": "1.653.0",
"windmill-parser-wasm-regex": "^1.670.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.647.1",
"windmill-parser-wasm-ts": "1.657.2",
@@ -844,7 +844,6 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -856,7 +855,6 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -867,7 +865,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1357,7 +1354,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1514,7 +1510,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1531,7 +1526,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1548,7 +1542,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1565,7 +1558,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1582,7 +1574,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1599,7 +1590,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1616,7 +1606,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1633,7 +1622,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1650,7 +1638,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1667,7 +1654,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1684,7 +1670,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1701,7 +1686,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1718,7 +1702,6 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1735,7 +1718,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1752,7 +1734,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2058,7 +2039,6 @@
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -6867,7 +6847,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -7366,7 +7346,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7387,7 +7366,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7408,7 +7386,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7429,7 +7406,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7450,7 +7426,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7471,7 +7446,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7492,7 +7466,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7513,7 +7486,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7534,7 +7506,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7555,7 +7526,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7576,7 +7546,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -12152,21 +12121,6 @@
}
}
},
"node_modules/svelte-check/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"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",
@@ -12897,7 +12851,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -13692,9 +13646,9 @@
"integrity": "sha512-5YNeUibxpNBvYrxCgQcz1PxGhTFx2CyEpg2udtIhq7bx0d4gF/KDZVupMeQmAObmrEtTSFGUWNRJ4zXSWNrSpQ=="
},
"node_modules/windmill-parser-wasm-regex": {
"version": "1.653.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.653.0.tgz",
"integrity": "sha512-LEvLhb8uCb/jGMzd+lwP886LcwxuTE+W04wdyqmdqy1JD9FBToKeiqW7CtZWqbWr1oI8yK9U3jmqSqyOcUCzRw=="
"version": "1.670.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.670.0.tgz",
"integrity": "sha512-foQBwLn7L3JpmPSzMYmFhpVvUnXB1KX4v1rGBsm5W6KircwrjW3VXl/Ih9Izjyju2RRK+tYzjN8vvd2Zo7ulKQ=="
},
"node_modules/windmill-parser-wasm-ruby": {
"version": "1.526.1",
+1 -1
View File
@@ -157,7 +157,7 @@
"windmill-parser-wasm-php": "1.647.1",
"windmill-parser-wasm-py": "1.657.2",
"windmill-parser-wasm-r": "1.668.1",
"windmill-parser-wasm-regex": "1.653.0",
"windmill-parser-wasm-regex": "^1.670.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.647.1",
"windmill-parser-wasm-ts": "1.657.2",
@@ -0,0 +1,89 @@
<script lang="ts">
import { Badge } from './common'
import Toggle from './Toggle.svelte'
import Select from '$lib/components/select/Select.svelte'
import { untrack } from 'svelte'
import { ChevronRight } from 'lucide-svelte'
interface Props {
args?: Record<string, any>
}
let { args = $bindable({}) }: Props = $props()
let verbose = $state(false)
let debug = $state(false)
let errorAction = $state(undefined as string | undefined)
let collapsed = $state(true)
let initialized = false
const errorActionItems = [
{ label: 'Stop', value: 'Stop' },
{ label: 'Continue', value: 'Continue' },
{ label: 'SilentlyContinue', value: 'SilentlyContinue' }
]
let activeBadges = $derived.by(() => {
const badges: string[] = []
if (verbose) badges.push('Verbose')
if (debug) badges.push('Debug')
if (errorAction) badges.push(`ErrorAction: ${errorAction}`)
return badges
})
// Initialize toggles from pre-populated args (e.g. "Run again")
$effect(() => {
if (!initialized && args && Object.keys(args).length > 0) {
initialized = true
untrack(() => {
verbose = args['_wm_ps_verbose'] === true
debug = args['_wm_ps_debug'] === true
errorAction = args['_wm_ps_error_action'] ?? undefined
})
}
})
// Sync toggles → args
$effect(() => {
const newArgs: Record<string, any> = {}
if (verbose) newArgs['_wm_ps_verbose'] = true
if (debug) newArgs['_wm_ps_debug'] = true
if (errorAction) newArgs['_wm_ps_error_action'] = errorAction
args = newArgs
})
</script>
<div class="flex flex-col">
<button
class="flex items-center gap-1 text-xs text-secondary hover:text-primary transition-colors"
onclick={() => (collapsed = !collapsed)}
>
<ChevronRight size={12} class="transition duration-200 {collapsed ? '' : 'rotate-90'}" />
CmdletBinding parameters
{#if collapsed && activeBadges.length > 0}
<div class="flex gap-1 ml-1">
{#each activeBadges as label}
<Badge color="blue">{label}</Badge>
{/each}
</div>
{/if}
</button>
{#if !collapsed}
<div class="flex flex-col gap-2 mt-2 ml-4">
<Toggle options={{ right: 'Verbose' }} bind:checked={verbose} size="xs" />
<Toggle options={{ right: 'Debug' }} bind:checked={debug} size="xs" />
<div class="flex items-center gap-2">
<span class="text-xs text-secondary">ErrorAction</span>
<div class="w-40">
<Select
items={errorActionItems}
bind:value={errorAction}
placeholder="Default"
clearable
size="sm"
/>
</div>
</div>
</div>
{/if}
</div>
+44 -1
View File
@@ -23,15 +23,35 @@
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
import { untrack } from 'svelte'
import { processSecretArgs } from './secretArgUtils'
import PowerShellCommonParams from './PowerShellCommonParams.svelte'
let reloadArgs = $state(0)
let jsonEditor: JsonInputs | undefined = $state(undefined)
let schemaHeight = $state(0)
let showInputSelectedBadge = $state(false)
let savedPreviousArgs: Record<string, any> | undefined = $state(undefined)
let psCommonParams: Record<string, any> = $state({})
function extractPsCommonParams(allArgs: Record<string, any>): {
scriptArgs: Record<string, any>
commonParams: Record<string, any>
} {
const scriptArgs: Record<string, any> = {}
const commonParams: Record<string, any> = {}
for (const [k, v] of Object.entries(allArgs)) {
if (k.startsWith('_wm_ps_')) {
commonParams[k] = v
} else {
scriptArgs[k] = v
}
}
return { scriptArgs, commonParams }
}
export async function setArgs(nargs: Record<string, any>) {
args = nargs
const { scriptArgs, commonParams } = extractPsCommonParams(nargs)
args = scriptArgs
psCommonParams = commonParams
reloadArgs++
}
@@ -46,6 +66,13 @@
sendUserToast('Failed to process sensitive args: ' + e, true)
return
}
if (showPsCommonParams) {
for (const [k, v] of Object.entries(psCommonParams)) {
if (v !== undefined && v !== false && v !== '') {
processedArgs[k] = v
}
}
}
runAction(
overrideScheduledForStr === null ? undefined : (overrideScheduledForStr ?? scheduledForStr),
processedArgs,
@@ -64,6 +91,7 @@
is_template?: boolean
hash?: string
kind?: string
language?: string
can_write?: boolean
created_at?: string
created_by?: string
@@ -109,10 +137,20 @@
isValid = $bindable(true)
}: Props = $props()
let showPsCommonParams = $derived(
runnable?.language === 'powershell' && runnable?.schema?.['x-windmill-ps-cmd-binding'] === true
)
$effect.pre(() => {
if (args == undefined) {
args = {}
}
// Extract _wm_ps_* keys from args on initial load (e.g. "Run again" via URL hash)
if (args && Object.keys(args).some((k) => k.startsWith('_wm_ps_'))) {
const { scriptArgs, commonParams } = extractPsCommonParams(args)
args = scriptArgs
psCommonParams = commonParams
}
})
let debounced: number | undefined = undefined
@@ -307,6 +345,11 @@
{:else}
<div class="text-xs text-primary">No arguments</div>
{/if}
{#if showPsCommonParams}
<div class="mt-4">
<PowerShellCommonParams bind:args={psCommonParams} />
</div>
{/if}
{#if schedulable}
<div class="flex gap-2 items-start flex-wrap justify-between mt-2 md:mt-6">
<div class="flex-row-reverse flex-wrap flex w-full gap-4">
@@ -24,6 +24,7 @@
import WacDiagram from '$lib/components/graph/WacDiagram.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import SchemaForm from './SchemaForm.svelte'
import PowerShellCommonParams from './PowerShellCommonParams.svelte'
import LogPanel from './scriptEditor/LogPanel.svelte'
import EditorBar, { EDITOR_BAR_WIDTH_THRESHOLD } from './EditorBar.svelte'
import JobLoader from './JobLoader.svelte'
@@ -188,6 +189,8 @@
let initialArgs = structuredClone($state.snapshot(args))
let jsonView = $state(false)
let schemaHeight = $state(0)
let psCommonParams: Record<string, any> = $state({})
let showPsCommonParams = $derived(lang === 'powershell' && /^\s*\[CmdletBinding/im.test(code))
// Module tab state
let activeModuleTab: string | null = $state(null)
@@ -654,6 +657,13 @@
: (args ?? {})
const testSchema = activeModuleTab !== null ? testPanelSchema : schema
const testArgs = await processSecretArgs(rawTestArgs, testSchema)
if (showPsCommonParams) {
for (const [k, v] of Object.entries(psCommonParams)) {
if (v !== undefined && v !== false && v !== '') {
testArgs[k] = v
}
}
}
//@ts-ignore
let job = await jobLoader.runPreview(
@@ -1616,6 +1626,13 @@
/>
{/if}
{/key}
{#if showPsCommonParams}
<div class="mt-2">
<PowerShellCommonParams
bind:args={psCommonParams}
/>
</div>
{/if}
</div>
</div>
{/if}
+14
View File
@@ -467,6 +467,20 @@ export async function inferArgs(
schema.required.push(arg.name)
}
}
// Store PowerShell CmdletBinding metadata as schema extensions
const psSchema = inferedSchema as MainArgSignature & {
has_cmd_binding?: boolean
supports_should_process?: boolean
}
if (language === 'powershell' && psSchema.has_cmd_binding) {
;(schema as any)['x-windmill-ps-cmd-binding'] = true
;(schema as any)['x-windmill-ps-supports-should-process'] =
psSchema.supports_should_process ?? false
} else {
delete (schema as any)['x-windmill-ps-cmd-binding']
delete (schema as any)['x-windmill-ps-supports-should-process']
}
await tick()
return {