fix(cli): phantom diffs, flow safety, trigger DX, lint watch, error clarity (#8588)

* fix(cli): phantom diffs, flow push safety, error messages, digest stability

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

* fix(cli): differentiate stale vs missing metadata warnings on script push

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

* fix(cli): job list --limit off-by-one, deps push double error

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

* fix(cli): flow get shows nested steps, lint works on specific directories

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

* feat(cli): add lint --watch mode for continuous validation

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

* fix(cli): email trigger template missing local_part, trigger get shows all fields

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

* fix(cli): fix CI — flow push warns instead of failing, lint subdir detection

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-03-28 20:56:10 +00:00
committed by GitHub
parent 37799574d8
commit c6ce3197a7
15 changed files with 218 additions and 111 deletions

View File

@@ -43,70 +43,66 @@ export async function pushWorkspaceDependencies(
_befObj: any,
newDependenciesContent: string,
): Promise<void> {
try {
const res = workspaceDependenciesPathToLanguageAndFilename(path);
if (!res) {
throw new Error(`Unknown workspace dependencies file format: ${path}`);
}
const { language, name } = res;
const displayName = name
? `named dependencies "${name}"`
: `workspace default dependencies`;
// Fetch remote workspace dependencies and compare content directly
try {
const remoteDeps = await wmill.getLatestWorkspaceDependencies({
workspace,
language,
name,
});
if (remoteDeps && remoteDeps.content === newDependenciesContent) {
log.info(
colors.green(
`${displayName} for ${language} are up-to-date, skipping push`,
),
);
return;
}
} catch (e: any) {
// If 404 or not found, the dependency doesn't exist remotely yet - proceed with push
if (e.status !== 404 && !e.message?.includes("not found")) {
throw e;
}
}
log.info(
colors.yellow(
`Pushing ${
name ? "named" : "workspace default"
} dependencies for ${language}...`,
),
const res = workspaceDependenciesPathToLanguageAndFilename(path);
if (!res) {
throw new Error(
`Unknown workspace dependencies file format: ${path}. ` +
`Valid files: package.json, requirements.in, composer.json, go.mod, modules.json`
);
}
await wmill.createWorkspaceDependencies({
const { language, name } = res;
const displayName = name
? `named dependencies "${name}"`
: `workspace default dependencies`;
// Fetch remote workspace dependencies and compare content directly
try {
const remoteDeps = await wmill.getLatestWorkspaceDependencies({
workspace,
requestBody: {
name,
content: newDependenciesContent,
language,
workspace_id: workspace,
// Description is not supported in cli, it will use old description
description: undefined,
},
language,
name,
});
log.info(
colors.green(`Successfully pushed ${displayName} for ${language}`),
);
} catch (error: any) {
log.error(
colors.red(`Failed to push workspace dependencies: ${error.message}`),
);
throw error;
if (remoteDeps && remoteDeps.content === newDependenciesContent) {
log.info(
colors.green(
`${displayName} for ${language} are up-to-date, skipping push`,
),
);
return;
}
} catch (e: any) {
// If 404 or not found, the dependency doesn't exist remotely yet - proceed with push
if (e.status !== 404 && !e.message?.includes("not found")) {
throw e;
}
}
log.info(
colors.yellow(
`Pushing ${
name ? "named" : "workspace default"
} dependencies for ${language}...`,
),
);
await wmill.createWorkspaceDependencies({
workspace,
requestBody: {
name,
content: newDependenciesContent,
language,
workspace_id: workspace,
// Description is not supported in cli, it will use old description
description: undefined,
},
});
log.info(
colors.green(`Successfully pushed ${displayName} for ${language}`),
);
}
export default command;

View File

@@ -154,18 +154,27 @@ export async function pushFlow(
const localFlow = (await yamlParseFile(localPath + "flow.yaml")) as FlowFile;
const fileReader = async (path: string) => await readFile(localPath + path, "utf-8");
const missingFiles: string[] = [];
await replaceInlineScripts(
localFlow.value.modules,
fileReader,
log,
localPath,
SEP
SEP,
undefined,
missingFiles
);
if (localFlow.value.failure_module) {
await replaceInlineScripts([localFlow.value.failure_module], fileReader, log, localPath, SEP);
await replaceInlineScripts([localFlow.value.failure_module], fileReader, log, localPath, SEP, undefined, missingFiles);
}
if (localFlow.value.preprocessor_module) {
await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, log, localPath, SEP);
await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, log, localPath, SEP, undefined, missingFiles);
}
if (missingFiles.length > 0) {
log.warn(colors.yellow(
`Warning: missing inline script file(s): ${missingFiles.join(", ")}. ` +
`The flow will be pushed with unresolved !inline references.`
));
}
if (flow) {
@@ -272,11 +281,26 @@ async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
const modules = (f as any).value?.modules;
if (modules && Array.isArray(modules) && modules.length > 0) {
console.log(colors.bold("Steps:"));
for (const mod of modules) {
const type = mod.value?.type ?? "unknown";
const detail = mod.value?.language ?? mod.value?.path ?? "";
console.log(` ${mod.id}: ${type}${detail ? " (" + detail + ")" : ""}`);
function printModules(mods: any[], indent: string = " ") {
for (const mod of mods) {
const type = mod.value?.type ?? "unknown";
const detail = mod.value?.language ?? mod.value?.path ?? "";
console.log(`${indent}${mod.id}: ${type}${detail ? " (" + detail + ")" : ""}`);
if (type === "branchall" || type === "branchone") {
for (const branch of mod.value?.branches ?? []) {
console.log(`${indent} Branch: ${branch.summary || "(default)"}`);
if (branch.modules) printModules(branch.modules, indent + " ");
}
if (type === "branchone" && mod.value?.default) {
console.log(`${indent} Default:`);
printModules(mod.value.default, indent + " ");
}
} else if (type === "forloopflow" || type === "whileloopflow") {
if (mod.value?.modules) printModules(mod.value.modules, indent + " ");
}
}
}
printModules(modules);
}
}
}
@@ -493,7 +517,6 @@ async function preview(
process.exitCode = 1;
return;
}
log.error(`Flow preview failed: ${JSON.stringify(e.body)}`);
}
throw e;
}

View File

@@ -67,17 +67,20 @@ async function list(
let successFilter = opts.success;
if (opts.failed) successFilter = false;
const jobs = await wmill.listJobs({
const limit = Math.min(opts.limit ?? 30, 100);
const allJobs = await wmill.listJobs({
workspace: workspace.workspaceId,
scriptPathExact: opts.scriptPath,
createdBy: opts.createdBy,
running: opts.running,
success: successFilter,
perPage: Math.min(opts.limit ?? 30, 100),
perPage: limit,
jobKinds: opts.jobKinds ?? "script,flow,singlestepflow",
label: opts.label,
hasNullParent: opts.all ? undefined : true,
});
// API may return more than perPage — enforce limit client-side
const jobs = allJobs.slice(0, limit);
if (opts.json) {
console.log(JSON.stringify(jobs));

View File

@@ -625,7 +625,13 @@ export async function runLint(
throw new Error(`Path is not a directory: ${targetDirectory}`);
}
const ignore = await ignoreF(mergedOpts);
// When the user specifies a subdirectory (that doesn't contain wmill.yaml),
// skip include/exclude filters since they're relative to the project root.
const isSubdirectory = explicitTargetDirectory &&
!(await stat(path.join(targetDirectory, "wmill.yaml")).catch(() => null));
const ignore = isSubdirectory
? (_p: string, _isDir: boolean) => false
: await ignoreF(mergedOpts);
const root = await FSFSElement(targetDirectory, [], false);
const validator = new WindmillYamlValidator();
@@ -640,9 +646,10 @@ export async function runLint(
if (entry.isDirectory || entry.ignored) {
continue;
}
scannedFiles += 1;
const normalizedPath = normalizePath(entry.path);
scannedFiles += 1;
if (!YAML_FILE_REGEX.test(normalizedPath)) {
continue;
}
@@ -742,7 +749,11 @@ export function printReport(report: LintReport, jsonOutput: boolean) {
}
}
async function lint(opts: LintOptions, directory?: string) {
async function lint(opts: LintOptions & { watch?: boolean }, directory?: string) {
if (opts.watch) {
await lintWatch(opts, directory);
return;
}
try {
const report = await runLint(opts, directory);
printReport(report, !!opts.json);
@@ -770,6 +781,37 @@ async function lint(opts: LintOptions, directory?: string) {
}
}
async function lintWatch(opts: LintOptions, directory?: string) {
const { watch } = await import("node:fs");
const targetDir = directory ? path.resolve(process.cwd(), directory) : process.cwd();
log.info(colors.blue(`Watching ${targetDir} for changes... (Ctrl+C to stop)`));
async function runAndReport() {
try {
const report = await runLint(opts, directory);
// Clear screen for readability
process.stdout.write("\x1Bc");
log.info(colors.gray(`[${new Date().toLocaleTimeString()}] Lint results:\n`));
printReport(report, false);
} catch (error) {
log.error(error instanceof Error ? error.message : String(error));
}
}
await runAndReport();
let debounce: ReturnType<typeof setTimeout> | null = null;
watch(targetDir, { recursive: true }, (_event, filename) => {
if (!filename || !filename.toString().endsWith(".yaml") && !filename.toString().endsWith(".yml")) return;
if (debounce) clearTimeout(debounce);
debounce = setTimeout(runAndReport, 300);
});
// Keep the process alive
await new Promise(() => {});
}
const command = new Command()
.description(
"Validate Windmill flow, schedule, and trigger YAML files in a directory",
@@ -781,6 +823,7 @@ const command = new Command()
"--locks-required",
"Fail if scripts or flow inline scripts that need locks have no locks",
)
.option("-w, --watch", "Watch for file changes and re-lint automatically")
.action(lint as any);
export default command;

View File

@@ -126,20 +126,25 @@ async function push(opts: PushOptions, filePath: string) {
await requireLogin(opts);
// Warn if metadata appears stale (content changed since last generate-metadata)
// Warn about metadata state before pushing
try {
const content = await readFile(filePath, "utf-8");
const remotePath = removeExtensionToPath(filePath).replaceAll(SEP, "/");
const contentHash = await generateHash(content + remotePath);
const conf = await readLockfile();
if (!(await checkifMetadataUptodate(remotePath, contentHash, conf))) {
const hasLockEntry = conf.locks && (conf.locks[remotePath] !== undefined || conf.locks[`${remotePath}.ts`] !== undefined);
if (!hasLockEntry) {
log.warn(colors.yellow(
`No metadata generated yet for ${filePath}. Run 'wmill generate-metadata' to generate schema and lock.`
));
} else if (!(await checkifMetadataUptodate(remotePath, contentHash, conf))) {
log.warn(colors.yellow(
`Metadata for ${filePath} appears stale (content changed since last 'wmill generate-metadata').\n` +
`The schema and lock may not match the current code. Consider running 'wmill generate-metadata' first.`
));
}
} catch {
// Don't block push if staleness check fails
// Don't block push if check fails
}
const codebases = await listSyncCodebases(opts as SyncOptions);
@@ -1584,11 +1589,11 @@ async function history(
const command = new Command()
.description("script related commands")
.option("--show-archived", "Enable archived scripts in output")
.option("--show-archived", "Show archived scripts instead of active ones")
.option("--json", "Output as JSON (for piping to jq)")
.action(list as any)
.command("list", "list all scripts")
.option("--show-archived", "Enable archived scripts in output")
.option("--show-archived", "Show archived scripts instead of active ones")
.option("--json", "Output as JSON (for piping to jq)")
.action(list as any)
.command(

View File

@@ -76,7 +76,7 @@ import {
newRawAppPathAssigner,
PathAssigner,
} from "../../../windmill-utils-internal/src/path-utils/path-assigner.ts";
import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts";
import { extractInlineScripts as extractInlineScriptsForFlows, extractCurrentMapping } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts";
import { generateFlowLockInternal } from "../flow/flow_metadata.ts";
import { isExecutionModeAnonymous } from "../app/app.ts";
import {
@@ -638,9 +638,16 @@ function ZipFSElement(
let inlineScripts;
try {
const assigner = newPathAssigner(defaultTs, { skipInlineScriptSuffix: getNonDottedPaths() });
inlineScripts = extractInlineScriptsForFlows(
// Preserve original !inline filenames from the flow to avoid phantom renames
const inlineMapping = extractCurrentMapping(
flow.value.modules as any,
{},
flow.value.failure_module,
flow.value.preprocessor_module,
);
inlineScripts = extractInlineScriptsForFlows(
flow.value.modules as any,
inlineMapping,
SEP,
defaultTs,
assigner,
@@ -649,7 +656,7 @@ function ZipFSElement(
if (flow.value.failure_module) {
inlineScripts.push(...extractInlineScriptsForFlows(
[flow.value.failure_module],
{},
inlineMapping,
SEP,
defaultTs,
assigner,
@@ -659,7 +666,7 @@ function ZipFSElement(
if (flow.value.preprocessor_module) {
inlineScripts.push(...extractInlineScriptsForFlows(
[flow.value.preprocessor_module],
{},
inlineMapping,
SEP,
defaultTs,
assigner,
@@ -1518,6 +1525,10 @@ async function compareDynFSElement(
continue;
}
if (k.startsWith("dependencies/")) {
if (!workspaceDependenciesPathToLanguageAndFilename(k)) {
log.warn(`Skipping unrecognized workspace dependencies file: ${k}`);
continue;
}
log.info(`Adding workspace dependencies file: ${k}`);
}
changes.push({ name: "added", path: k, content: v });

View File

@@ -378,6 +378,7 @@ const triggerTemplates: Record<TriggerType, Record<string, any>> = {
email: {
script_path: "",
is_flow: false,
local_part: "",
enabled: false,
},
};
@@ -409,6 +410,27 @@ async function newTrigger(opts: GlobalOptions & { kind: string }, path: string)
log.info(colors.green(`Created ${filePath}`));
}
const TRIGGER_SKIP_FIELDS = new Set(["workspace_id", "extra_perms", "edited_by", "edited_at"]);
function printTriggerDetails(trigger: any, kind: string) {
console.log(colors.bold("Path:") + " " + trigger.path);
console.log(colors.bold("Kind:") + " " + kind);
console.log(colors.bold("Enabled:") + " " + (trigger.enabled ?? trigger.mode ?? "-"));
console.log(colors.bold("Script Path:") + " " + (trigger.script_path ?? ""));
console.log(colors.bold("Is Flow:") + " " + (trigger.is_flow ? "true" : "false"));
// Show all other non-internal fields
for (const [key, value] of Object.entries(trigger)) {
if (["path", "enabled", "mode", "script_path", "is_flow"].includes(key)) continue;
if (TRIGGER_SKIP_FIELDS.has(key)) continue;
if (value === undefined || value === null || value === "") continue;
const display = Array.isArray(value) ? (value.length > 0 ? JSON.stringify(value) : "[]") :
typeof value === "object" ? JSON.stringify(value) : String(value);
if (display === "[]" || display === "{}") continue;
const label = key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
console.log(colors.bold(label + ":") + " " + display);
}
}
async function get(opts: GlobalOptions & { json?: boolean; kind?: string }, path: string) {
if (opts.json) log.setSilent(true);
const workspace = await resolveWorkspace(opts);
@@ -422,11 +444,7 @@ async function get(opts: GlobalOptions & { json?: boolean; kind?: string }, path
if (opts.json) {
console.log(JSON.stringify(trigger));
} else {
console.log(colors.bold("Path:") + " " + (trigger as any).path);
console.log(colors.bold("Kind:") + " " + opts.kind);
console.log(colors.bold("Enabled:") + " " + ((trigger as any).enabled ?? "-"));
console.log(colors.bold("Script Path:") + " " + ((trigger as any).script_path ?? ""));
console.log(colors.bold("Is Flow:") + " " + ((trigger as any).is_flow ? "true" : "false"));
printTriggerDetails(trigger as any, opts.kind);
}
return;
}
@@ -451,11 +469,7 @@ async function get(opts: GlobalOptions & { json?: boolean; kind?: string }, path
if (opts.json) {
console.log(JSON.stringify(trigger));
} else {
console.log(colors.bold("Path:") + " " + trigger.path);
console.log(colors.bold("Kind:") + " " + kind);
console.log(colors.bold("Enabled:") + " " + (trigger.enabled ?? (trigger as any).mode ?? "-"));
console.log(colors.bold("Script Path:") + " " + (trigger.script_path ?? ""));
console.log(colors.bold("Is Flow:") + " " + (trigger.is_flow ? "true" : "false"));
printTriggerDetails(trigger, kind);
}
return;
}

View File

@@ -550,7 +550,7 @@ export async function resolveWorkspace(
}
// If everything failed, show error
log.info(colors.red.bold("No workspace given and no default set."));
log.info(colors.red.bold("No workspace given and no default set. Run 'wmill workspace add' to configure one."));
return process.exit(-1);
}

View File

@@ -5260,6 +5260,7 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
- \`--json\` - Output results in JSON format
- \`--fail-on-warn\` - Exit with code 1 when warnings are emitted
- \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks
- \`-w, --watch\` - Watch for file changes and re-lint automatically
### queues
@@ -5328,13 +5329,13 @@ schedule related commands
script related commands
**Options:**
- \`--show-archived\` - Enable archived scripts in output
- \`--show-archived\` - Show archived scripts instead of active ones
- \`--json\` - Output as JSON (for piping to jq)
**Subcommands:**
- \`script list\` - list all scripts
- \`--show-archived\` - Enable archived scripts in output
- \`--show-archived\` - Show archived scripts instead of active ones
- \`--json\` - Output as JSON (for piping to jq)
- \`script push <path:file>\` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
- \`--message <message:string>\` - Deployment message

View File

@@ -231,7 +231,9 @@ async function main() {
} catch (e) {
if (e && typeof e === "object" && "name" in e && e.name === "ApiError") {
const body = (e as any).body;
const bodyStr = typeof body === "object" && body !== null ? JSON.stringify(body) : body;
let bodyStr = typeof body === "object" && body !== null ? JSON.stringify(body) : String(body ?? "");
// Strip backend source file references like (flows.rs:1400) or @scripts.rs:123:45
bodyStr = bodyStr.replace(/\s*[@(]\w+\.rs:\d+[:\d]*\)?/g, "");
log.error(
"Server failed. " + (e as any).statusText + ": " + bodyStr
);

View File

@@ -107,6 +107,7 @@ export function getHeaders(): Record<string, string> | undefined {
export async function digestDir(path: string, conf: string) {
const hashes: string = [];
const entries = await readdir(path, { withFileTypes: true });
entries.sort((a, b) => a.name.localeCompare(b.name));
for (const e of entries) {
const npath = path + "/" + e.name;
if (e.isFile()) {

View File

@@ -13,7 +13,8 @@ async function replaceRawscriptInline(
fileReader: (path: string) => Promise<string>,
logger: { info: (message: string) => void; error: (message: string) => void },
separator: string,
removeLocks?: string[]
removeLocks?: string[],
missingFiles?: string[]
): Promise<void> {
if (!rawscript.content || !rawscript.content.startsWith("!inline")) {
return;
@@ -31,6 +32,7 @@ async function replaceRawscriptInline(
rawscript.content = await fileReader(newPath);
} catch {
logger.error(`Script file ${newPath} not found`);
if (missingFiles) missingFiles.push(path);
}
}
@@ -76,14 +78,14 @@ export async function replaceInlineScripts(
localPath: string,
separator: string = "/",
removeLocks?: string[],
// renamer?: (path: string, newPath: string) => void,
// deleter?: (path: string) => void
): Promise<void> {
missingFiles?: string[],
): Promise<string[]> {
const missing = missingFiles ?? [];
await Promise.all(modules.map(async (module) => {
if (!module.value) {
throw new Error(`Module value is undefined for module ${module.id}`);
}
if (module.value.type === "rawscript") {
await replaceRawscriptInline(
module.id,
@@ -91,19 +93,20 @@ export async function replaceInlineScripts(
fileReader,
logger,
separator,
removeLocks
removeLocks,
missing
);
} else if (module.value.type === "forloopflow" || module.value.type === "whileloopflow") {
await replaceInlineScripts(module.value.modules, fileReader, logger, localPath, separator, removeLocks);
await replaceInlineScripts(module.value.modules, fileReader, logger, localPath, separator, removeLocks, missing);
} else if (module.value.type === "branchall") {
await Promise.all(module.value.branches.map(async (branch) => {
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, separator, removeLocks);
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, separator, removeLocks, missing);
}));
} else if (module.value.type === "branchone") {
await Promise.all(module.value.branches.map(async (branch) => {
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, separator, removeLocks);
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, separator, removeLocks, missing);
}));
await replaceInlineScripts(module.value.default, fileReader, logger, localPath, separator, removeLocks);
await replaceInlineScripts(module.value.default, fileReader, logger, localPath, separator, removeLocks, missing);
} else if (module.value.type === "aiagent") {
await Promise.all((module.value.tools ?? []).map(async (tool) => {
const toolValue = tool.value;
@@ -120,11 +123,13 @@ export async function replaceInlineScripts(
fileReader,
logger,
separator,
removeLocks
removeLocks,
missing
);
}));
}
}));
return missing;
}
/**

View File

@@ -302,6 +302,7 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
- `--json` - Output results in JSON format
- `--fail-on-warn` - Exit with code 1 when warnings are emitted
- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks
- `-w, --watch` - Watch for file changes and re-lint automatically
### queues
@@ -370,13 +371,13 @@ schedule related commands
script related commands
**Options:**
- `--show-archived` - Enable archived scripts in output
- `--show-archived` - Show archived scripts instead of active ones
- `--json` - Output as JSON (for piping to jq)
**Subcommands:**
- `script list` - list all scripts
- `--show-archived` - Enable archived scripts in output
- `--show-archived` - Show archived scripts instead of active ones
- `--json` - Output as JSON (for piping to jq)
- `script push <path:file>` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
- `--message <message:string>` - Deployment message

View File

@@ -1829,6 +1829,7 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
- \`--json\` - Output results in JSON format
- \`--fail-on-warn\` - Exit with code 1 when warnings are emitted
- \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks
- \`-w, --watch\` - Watch for file changes and re-lint automatically
### queues
@@ -1897,13 +1898,13 @@ schedule related commands
script related commands
**Options:**
- \`--show-archived\` - Enable archived scripts in output
- \`--show-archived\` - Show archived scripts instead of active ones
- \`--json\` - Output as JSON (for piping to jq)
**Subcommands:**
- \`script list\` - list all scripts
- \`--show-archived\` - Enable archived scripts in output
- \`--show-archived\` - Show archived scripts instead of active ones
- \`--json\` - Output as JSON (for piping to jq)
- \`script push <path:file>\` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
- \`--message <message:string>\` - Deployment message

View File

@@ -307,6 +307,7 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
- `--json` - Output results in JSON format
- `--fail-on-warn` - Exit with code 1 when warnings are emitted
- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks
- `-w, --watch` - Watch for file changes and re-lint automatically
### queues
@@ -375,13 +376,13 @@ schedule related commands
script related commands
**Options:**
- `--show-archived` - Enable archived scripts in output
- `--show-archived` - Show archived scripts instead of active ones
- `--json` - Output as JSON (for piping to jq)
**Subcommands:**
- `script list` - list all scripts
- `--show-archived` - Enable archived scripts in output
- `--show-archived` - Show archived scripts instead of active ones
- `--json` - Output as JSON (for piping to jq)
- `script push <path:file>` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
- `--message <message:string>` - Deployment message