mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 16:05:42 +00:00
feat(cli): add object-storage commands and flow test-step (#9326)
* feat(cli): add object-storage commands and flow test-step * docs(cli): clarify flow test-step doesn't recurse into aiagent tools * fix(cli): correct failure step id in docs, handle bare flow.yaml path
This commit is contained in:
@@ -4,7 +4,7 @@ import { Command } from "@cliffy/command";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { Table } from "@cliffy/table";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { dirname, sep as SEP } from "node:path";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { readTextFile, validateRequiredArgs } from "../../utils/utils.ts";
|
||||
@@ -562,7 +562,10 @@ async function preview(
|
||||
if (!isFlowDir) {
|
||||
// Check if it's a flow.yaml file
|
||||
if (flowPath.endsWith("flow.yaml") || flowPath.endsWith("flow.json")) {
|
||||
flowPath = flowPath.substring(0, flowPath.lastIndexOf(SEP));
|
||||
// Use dirname so a bare "flow.yaml" (no parent dir) becomes "."
|
||||
// instead of "" — the latter, after appending SEP below, becomes "/"
|
||||
// and silently reads from filesystem root.
|
||||
flowPath = dirname(flowPath);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Flow path must be a .flow/__flow directory or a flow.yaml file"
|
||||
@@ -674,6 +677,185 @@ async function preview(
|
||||
}
|
||||
}
|
||||
|
||||
function findStepInFlowValue(flowValue: any, stepId: string): any | undefined {
|
||||
if (!flowValue) return undefined;
|
||||
if (flowValue.failure_module?.id === stepId) return flowValue.failure_module;
|
||||
if (flowValue.preprocessor_module?.id === stepId) return flowValue.preprocessor_module;
|
||||
return findStepInModules(flowValue.modules ?? [], stepId);
|
||||
}
|
||||
|
||||
function findStepInModules(modules: any[], stepId: string): any | undefined {
|
||||
for (const m of modules) {
|
||||
if (m?.id === stepId) return m;
|
||||
const v = m?.value;
|
||||
if (!v) continue;
|
||||
if (v.type === "forloopflow" || v.type === "whileloopflow") {
|
||||
const found = findStepInModules(v.modules ?? [], stepId);
|
||||
if (found) return found;
|
||||
} else if (v.type === "branchone") {
|
||||
for (const b of v.branches ?? []) {
|
||||
const found = findStepInModules(b.modules ?? [], stepId);
|
||||
if (found) return found;
|
||||
}
|
||||
const found = findStepInModules(v.default ?? [], stepId);
|
||||
if (found) return found;
|
||||
} else if (v.type === "branchall") {
|
||||
for (const b of v.branches ?? []) {
|
||||
const found = findStepInModules(b.modules ?? [], stepId);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function collectStepIds(flowValue: any): string[] {
|
||||
const ids: string[] = [];
|
||||
const walkModules = (modules: any[]) => {
|
||||
for (const m of modules) {
|
||||
if (m?.id) ids.push(m.id);
|
||||
const v = m?.value;
|
||||
if (!v) continue;
|
||||
if (v.type === "forloopflow" || v.type === "whileloopflow") {
|
||||
walkModules(v.modules ?? []);
|
||||
} else if (v.type === "branchone") {
|
||||
for (const b of v.branches ?? []) walkModules(b.modules ?? []);
|
||||
walkModules(v.default ?? []);
|
||||
} else if (v.type === "branchall") {
|
||||
for (const b of v.branches ?? []) walkModules(b.modules ?? []);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (flowValue?.preprocessor_module?.id) ids.push(flowValue.preprocessor_module.id);
|
||||
if (flowValue?.failure_module?.id) ids.push(flowValue.failure_module.id);
|
||||
walkModules(flowValue?.modules ?? []);
|
||||
return ids;
|
||||
}
|
||||
|
||||
async function testStep(
|
||||
opts: GlobalOptions & {
|
||||
data?: string;
|
||||
silent: boolean;
|
||||
json?: boolean;
|
||||
} & SyncOptions,
|
||||
flowPath: string,
|
||||
stepId: string
|
||||
) {
|
||||
if (opts.silent || opts.json) log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// Normalize flow path (same logic as `preview`).
|
||||
const isFlowDir = flowPath.endsWith(".flow") || flowPath.endsWith(".flow" + SEP)
|
||||
|| flowPath.endsWith("__flow") || flowPath.endsWith("__flow" + SEP);
|
||||
if (!isFlowDir) {
|
||||
if (flowPath.endsWith("flow.yaml") || flowPath.endsWith("flow.json")) {
|
||||
// Use dirname so a bare "flow.yaml" (no parent dir) becomes "."
|
||||
// instead of "" — the latter, after appending SEP below, becomes "/"
|
||||
// and silently reads from filesystem root.
|
||||
flowPath = dirname(flowPath);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Flow path must be a .flow/__flow directory or a flow.yaml file"
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!flowPath.endsWith(SEP)) flowPath += SEP;
|
||||
|
||||
const localFlow = (await yamlParseFile(flowPath + "flow.yaml")) as FlowFile;
|
||||
const fileReader = async (path: string) => await readTextFile(flowPath + path);
|
||||
await replaceInlineScripts(localFlow.value.modules, fileReader, log, flowPath, SEP);
|
||||
if (localFlow.value.failure_module) {
|
||||
await replaceInlineScripts([localFlow.value.failure_module], fileReader, log, flowPath, SEP);
|
||||
}
|
||||
if (localFlow.value.preprocessor_module) {
|
||||
await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, log, flowPath, SEP);
|
||||
}
|
||||
|
||||
const module = findStepInFlowValue(localFlow.value, stepId);
|
||||
if (!module) {
|
||||
const available = collectStepIds(localFlow.value).join(", ") || "(none)";
|
||||
throw new Error(`Step '${stepId}' not found in flow. Available steps: ${available}`);
|
||||
}
|
||||
|
||||
const baseArgs = opts.data ? await resolve(opts.data) : {};
|
||||
const args =
|
||||
stepId === "preprocessor"
|
||||
? { _ENTRYPOINT_OVERRIDE: "preprocessor", ...baseArgs }
|
||||
: baseArgs;
|
||||
|
||||
const moduleValue = module.value;
|
||||
let jobId: string;
|
||||
if (moduleValue?.type === "rawscript") {
|
||||
if (!opts.silent && !opts.json) {
|
||||
log.info(colors.yellow(`Testing rawscript step '${stepId}' (${moduleValue.language})...`));
|
||||
}
|
||||
jobId = await wmill.runScriptPreview({
|
||||
workspace: workspace.workspaceId,
|
||||
requestBody: {
|
||||
content: moduleValue.content ?? "",
|
||||
language: moduleValue.language,
|
||||
args,
|
||||
},
|
||||
});
|
||||
} else if (moduleValue?.type === "script") {
|
||||
if (!opts.silent && !opts.json) {
|
||||
log.info(colors.yellow(`Testing script step '${stepId}' (${moduleValue.path})...`));
|
||||
}
|
||||
const script = moduleValue.hash
|
||||
? await wmill.getScriptByHash({
|
||||
workspace: workspace.workspaceId,
|
||||
hash: moduleValue.hash,
|
||||
})
|
||||
: await wmill.getScriptByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path: moduleValue.path,
|
||||
});
|
||||
jobId = await wmill.runScriptPreview({
|
||||
workspace: workspace.workspaceId,
|
||||
requestBody: {
|
||||
content: script.content,
|
||||
language: script.language as any,
|
||||
args,
|
||||
},
|
||||
});
|
||||
} else if (moduleValue?.type === "flow") {
|
||||
if (!opts.silent && !opts.json) {
|
||||
log.info(colors.yellow(`Testing flow step '${stepId}' (${moduleValue.path})...`));
|
||||
}
|
||||
jobId = await wmill.runFlowByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path: moduleValue.path,
|
||||
requestBody: args,
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot test step of type '${moduleValue?.type ?? "unknown"}'. Supported types: rawscript, script, flow.`
|
||||
);
|
||||
}
|
||||
|
||||
const { result, success } = await pollForJobResult(workspace.workspaceId, jobId);
|
||||
|
||||
if (!success) {
|
||||
if (opts.silent || opts.json) {
|
||||
console.log(JSON.stringify(result));
|
||||
} else {
|
||||
log.info(colors.yellow.bold(`Step '${stepId}' failed:`));
|
||||
log.info(JSON.stringify(result, null, 2));
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.silent || opts.json) {
|
||||
console.log(JSON.stringify(result));
|
||||
} else {
|
||||
log.info(colors.bold.underline.green(`Step '${stepId}' completed`));
|
||||
log.info(JSON.stringify(result, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateLocks(
|
||||
opts: GlobalOptions & {
|
||||
yes?: boolean;
|
||||
@@ -906,6 +1088,21 @@ const command = new Command()
|
||||
"Use deployed workspace scripts for PathScript steps instead of local files."
|
||||
)
|
||||
.action(preview as any)
|
||||
.command(
|
||||
"test-step",
|
||||
"Test a single step of a local flow in isolation. Resolves the step by id (including nested branchone/branchall/forloopflow/whileloopflow and the failure/preprocessor modules), runs only that step's runnable. Supported step types: rawscript, script (PathScript), flow (PathFlow)."
|
||||
)
|
||||
.arguments("<flow_path:string> <step_id:string>")
|
||||
.option(
|
||||
"-d --data <data:string>",
|
||||
"Step inputs as a JSON string or a file using @<filename> or stdin using @-."
|
||||
)
|
||||
.option(
|
||||
"-s --silent",
|
||||
"Do not output anything other then the final output. Useful for scripting."
|
||||
)
|
||||
.option("--json", "Output the result as JSON (same as --silent)")
|
||||
.action(testStep as any)
|
||||
.command(
|
||||
"generate-locks",
|
||||
'DEPRECATED: re-generate flow lock files. Use "wmill generate-metadata" instead.'
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { basename } from "node:path";
|
||||
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace } from "../../core/context.ts";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { Table } from "@cliffy/table";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "../../core/log.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { formatTimestamp } from "../../utils/utils.ts";
|
||||
|
||||
function formatBytes(n: number | undefined): string {
|
||||
if (n == null) return "-";
|
||||
if (n < 1024) return `${n}B`;
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}K`;
|
||||
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)}M`;
|
||||
return `${(n / (1024 * 1024 * 1024)).toFixed(2)}G`;
|
||||
}
|
||||
|
||||
async function listStorages(
|
||||
opts: GlobalOptions & { json?: boolean }
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const names = await wmill.getSecondaryStorageNames({
|
||||
workspace: workspace.workspaceId,
|
||||
includeDefault: true,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(names));
|
||||
return;
|
||||
}
|
||||
if (names.length === 0) {
|
||||
log.info("No object storage configured for this workspace.");
|
||||
return;
|
||||
}
|
||||
for (const name of names) {
|
||||
console.log(name === "_default_" ? `${name} ${colors.dim("(default)")}` : name);
|
||||
}
|
||||
}
|
||||
|
||||
async function listFiles(
|
||||
opts: GlobalOptions & {
|
||||
json?: boolean;
|
||||
maxKeys?: number;
|
||||
marker?: string;
|
||||
storage?: string;
|
||||
},
|
||||
prefix?: string
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const result = await wmill.listStoredFiles({
|
||||
workspace: workspace.workspaceId,
|
||||
maxKeys: opts.maxKeys ?? 100,
|
||||
marker: opts.marker,
|
||||
prefix,
|
||||
storage: opts.storage,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(result));
|
||||
return;
|
||||
}
|
||||
const files = result.windmill_large_files ?? [];
|
||||
if (files.length === 0) {
|
||||
log.info("No files found.");
|
||||
return;
|
||||
}
|
||||
new Table()
|
||||
.header(["Key"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(files.map((f) => [f.s3]))
|
||||
.render();
|
||||
if (result.next_marker) {
|
||||
log.info(`\nMore results available. Use --marker '${result.next_marker}' to paginate.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function upload(
|
||||
opts: GlobalOptions & {
|
||||
storage?: string;
|
||||
contentType?: string;
|
||||
contentDisposition?: string;
|
||||
},
|
||||
localPath: string,
|
||||
fileKey: string
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const buf = await readFile(localPath);
|
||||
// Wrap Node Buffer in a Blob for the SDK request body.
|
||||
const blob = new Blob([buf], { type: opts.contentType ?? "application/octet-stream" });
|
||||
|
||||
await wmill.fileUpload({
|
||||
workspace: workspace.workspaceId,
|
||||
fileKey,
|
||||
storage: opts.storage,
|
||||
contentType: opts.contentType,
|
||||
contentDisposition: opts.contentDisposition,
|
||||
requestBody: blob,
|
||||
});
|
||||
log.info(colors.green(`Uploaded ${localPath} -> ${fileKey}`));
|
||||
}
|
||||
|
||||
async function download(
|
||||
opts: GlobalOptions & { storage?: string; stdout?: boolean },
|
||||
fileKey: string,
|
||||
outputPath?: string
|
||||
) {
|
||||
if (opts.stdout) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// The generated request layer (cli/gen/core/request.ts:getResponseBody)
|
||||
// routes by Content-Type: binary types → Blob, text/* → string, JSON → object.
|
||||
// The generated return type is `Blob | File`, which is wrong for non-binary
|
||||
// responses, so widen to unknown before normalizing.
|
||||
const body: unknown = await wmill.fileDownload({
|
||||
workspace: workspace.workspaceId,
|
||||
fileKey,
|
||||
storage: opts.storage,
|
||||
});
|
||||
let buf: Buffer;
|
||||
if (typeof body === "string") {
|
||||
buf = Buffer.from(body, "utf-8");
|
||||
} else if (body instanceof Blob) {
|
||||
buf = Buffer.from(await body.arrayBuffer());
|
||||
} else if (body instanceof ArrayBuffer) {
|
||||
buf = Buffer.from(body);
|
||||
} else if (body == null) {
|
||||
buf = Buffer.alloc(0);
|
||||
} else {
|
||||
buf = Buffer.from(JSON.stringify(body), "utf-8");
|
||||
}
|
||||
|
||||
if (opts.stdout) {
|
||||
process.stdout.write(buf);
|
||||
return;
|
||||
}
|
||||
const dest = outputPath ?? basename(fileKey);
|
||||
await writeFile(dest, buf);
|
||||
log.info(colors.green(`Downloaded ${fileKey} -> ${dest}`));
|
||||
}
|
||||
|
||||
async function del(
|
||||
opts: GlobalOptions & { storage?: string; yes?: boolean },
|
||||
fileKey: string
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
if (!opts.yes) {
|
||||
const confirmed = await Confirm.prompt({
|
||||
message: `Delete '${fileKey}' from object storage${opts.storage ? ` (storage: ${opts.storage})` : ""}?`,
|
||||
default: false,
|
||||
});
|
||||
if (!confirmed) {
|
||||
log.info("Aborted.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await wmill.deleteS3File({
|
||||
workspace: workspace.workspaceId,
|
||||
fileKey,
|
||||
storage: opts.storage,
|
||||
});
|
||||
log.info(colors.green(`Deleted ${fileKey}`));
|
||||
}
|
||||
|
||||
async function move(
|
||||
opts: GlobalOptions & { storage?: string },
|
||||
srcFileKey: string,
|
||||
destFileKey: string
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await wmill.moveS3File({
|
||||
workspace: workspace.workspaceId,
|
||||
srcFileKey,
|
||||
destFileKey,
|
||||
storage: opts.storage,
|
||||
});
|
||||
log.info(colors.green(`Moved ${srcFileKey} -> ${destFileKey}`));
|
||||
}
|
||||
|
||||
async function info(
|
||||
opts: GlobalOptions & { json?: boolean; storage?: string },
|
||||
fileKey: string
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const metadata = await wmill.loadFileMetadata({
|
||||
workspace: workspace.workspaceId,
|
||||
fileKey,
|
||||
storage: opts.storage,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(metadata));
|
||||
return;
|
||||
}
|
||||
console.log(colors.bold("Key:") + " " + fileKey);
|
||||
console.log(colors.bold("Size:") + " " + formatBytes(metadata.size_in_bytes));
|
||||
console.log(colors.bold("Mime:") + " " + (metadata.mime_type ?? "-"));
|
||||
console.log(
|
||||
colors.bold("Last Modified:") + " " +
|
||||
(metadata.last_modified ? formatTimestamp(metadata.last_modified) : "-")
|
||||
);
|
||||
if (metadata.expires) {
|
||||
console.log(colors.bold("Expires:") + " " + formatTimestamp(metadata.expires));
|
||||
}
|
||||
if (metadata.version_id) {
|
||||
console.log(colors.bold("Version Id:") + " " + metadata.version_id);
|
||||
}
|
||||
}
|
||||
|
||||
async function preview(
|
||||
opts: GlobalOptions & {
|
||||
storage?: string;
|
||||
bytesFrom?: number;
|
||||
bytesLength?: number;
|
||||
csvSeparator?: string;
|
||||
csvHeader?: boolean;
|
||||
mime?: string;
|
||||
},
|
||||
fileKey: string
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// Backend requires both byte fields; mirror the frontend's defaults
|
||||
// (frontend/src/lib/components/S3FilePickerInner.svelte) for an interactive
|
||||
// peek so the user gets useful output without passing flags.
|
||||
const result = await wmill.loadFilePreview({
|
||||
workspace: workspace.workspaceId,
|
||||
fileKey,
|
||||
storage: opts.storage,
|
||||
fileMimeType: opts.mime,
|
||||
readBytesFrom: opts.bytesFrom ?? 0,
|
||||
readBytesLength: opts.bytesLength ?? 128 * 1024,
|
||||
csvSeparator: opts.csvSeparator,
|
||||
csvHasHeader: opts.csvHeader,
|
||||
});
|
||||
|
||||
if (result.msg) {
|
||||
log.info(colors.yellow(result.msg));
|
||||
}
|
||||
if (result.content != null) {
|
||||
process.stdout.write(result.content);
|
||||
if (!result.content.endsWith("\n")) process.stdout.write("\n");
|
||||
}
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.alias("s3")
|
||||
.description("Object storage (S3) related commands. Operates on the workspace's default object storage; use --storage to target a configured secondary storage.")
|
||||
.action(listStorages as any)
|
||||
.command(
|
||||
"list",
|
||||
"List configured object storages for the workspace (default + secondary)."
|
||||
)
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(listStorages as any)
|
||||
.command(
|
||||
"files",
|
||||
"List files in an object storage. Optionally filter by prefix."
|
||||
)
|
||||
.alias("ls")
|
||||
.arguments("[prefix:string]")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.option("--max-keys <maxKeys:number>", "Page size (default 100)")
|
||||
.option("--marker <marker:string>", "Pagination marker from a previous response")
|
||||
.option("--storage <storage:string>", "Secondary storage name (omit for the workspace default)")
|
||||
.action(listFiles as any)
|
||||
.command(
|
||||
"upload",
|
||||
"Upload a local file to object storage at the given file key."
|
||||
)
|
||||
.arguments("<local_path:string> <file_key:string>")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.option("--content-type <contentType:string>", "Content-Type header to set on the object")
|
||||
.option("--content-disposition <contentDisposition:string>", "Content-Disposition header to set on the object")
|
||||
.action(upload as any)
|
||||
.command(
|
||||
"download",
|
||||
"Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory."
|
||||
)
|
||||
.arguments("<file_key:string> [output_path:string]")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.option("--stdout", "Write file contents to stdout instead of a file")
|
||||
.action(download as any)
|
||||
.command(
|
||||
"delete",
|
||||
"Delete an object from object storage. Prompts for confirmation unless --yes is set."
|
||||
)
|
||||
.arguments("<file_key:string>")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.option("--yes", "Skip the confirmation prompt")
|
||||
.action(del as any)
|
||||
.command(
|
||||
"move",
|
||||
"Move an object within the same storage (rename or relocate by key)."
|
||||
)
|
||||
.arguments("<src_file_key:string> <dest_file_key:string>")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.action(move as any)
|
||||
.command(
|
||||
"info",
|
||||
"Show metadata (size, mime, last-modified) for an object."
|
||||
)
|
||||
.arguments("<file_key:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.action(info as any)
|
||||
.command(
|
||||
"preview",
|
||||
"Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files."
|
||||
)
|
||||
.arguments("<file_key:string>")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.option("--mime <mime:string>", "Override the detected mime type (e.g. text/csv)")
|
||||
.option("--bytes-from <bytesFrom:number>", "Start offset in bytes")
|
||||
.option("--bytes-length <bytesLength:number>", "Number of bytes to read")
|
||||
.option("--csv-separator <csvSeparator:string>", "CSV column separator (default ,)")
|
||||
.option("--csv-header", "Treat the first CSV row as a header")
|
||||
.action(preview as any);
|
||||
|
||||
export default command;
|
||||
@@ -5168,6 +5168,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- \`wmill flow preview <flow_path>\` — **default when iterating on a local flow.** Runs the local \`flow.yaml\` against local inline scripts without deploying. Add \`--remote\` to use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- \`wmill flow test-step <flow_path> <step_id>\` — runs a single step of the local flow in isolation. Use when iterating on one module (rawscript / script / flow types) and you don't want to wait for upstream steps. Supports nested steps inside branchone/branchall/forloopflow/whileloopflow, plus the special \`preprocessor\` and \`failure\` modules by id. Pass step args with \`-d '<json>'\`.
|
||||
- \`wmill flow run <path>\` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- \`wmill generate-metadata\` — regenerate stale \`.lock\` and \`.script.yaml\` files. By default it scans **scripts, flows, and apps** across the workspace; pass \`--skip-flows --skip-apps\` (or run from a subdirectory) to limit the scope when you only care about the flow you edited.
|
||||
- \`wmill sync push\` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
@@ -5184,6 +5185,12 @@ Only use \`sync push\` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### Test a single step vs preview the whole flow
|
||||
|
||||
Use \`flow test-step <flow_path> <step_id>\` when the user is iterating on one module and the flow's upstream steps aren't part of what they're trying to validate. It runs only that step's runnable (rawscript: the inline script; script: the PathScript fetched from the workspace; flow: the subflow by path) and is much faster than running the whole flow when previous steps are slow or expensive.
|
||||
|
||||
Use \`flow preview <flow_path>\` when steps depend on each other's outputs, when the user is validating the overall control flow, or when \`test-step\` doesn't apply (branchone, branchall, forloopflow, whileloopflow, identity, and AI agent steps cannot themselves be tested in isolation — for branchone/branchall/forloopflow/whileloopflow, the *contained* steps can, by passing the inner step's id).
|
||||
|
||||
### After writing — offer to run, don't wait passively
|
||||
|
||||
This is about **programmatic execution** (\`wmill flow preview -d '<args>'\`), which actually runs the flow and has side effects. Visual preview (the \`preview\` skill) is offered separately — see "Visual preview" below.
|
||||
@@ -6842,6 +6849,10 @@ flow related commands
|
||||
- \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not output anything other then the final output. Useful for scripting.
|
||||
- \`--remote\` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- \`flow test-step <flow_path:string> <step_id:string>\` - Test a single step of a local flow in isolation. Resolves the step by id (including nested branchone/branchall/forloopflow/whileloopflow and the failure/preprocessor modules), runs only that step's runnable. Supported step types: rawscript, script (PathScript), flow (PathFlow).
|
||||
- \`-d --data <data:string>\` - Step inputs as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not output anything other then the final output. Useful for scripting.
|
||||
- \`--json\` - Output the result as JSON (same as --silent)
|
||||
- \`flow new <flow_path:string>\` - create a new empty flow
|
||||
- \`--summary <summary:string>\` - flow summary
|
||||
- \`--description <description:string>\` - flow description
|
||||
@@ -7057,6 +7068,42 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- \`--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
|
||||
|
||||
### object-storage
|
||||
|
||||
**Alias:** \`s3\`
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`object-storage list\` - List configured object storages for the workspace (default + secondary).
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`object-storage files [prefix:string]\` - List files in an object storage. Optionally filter by prefix.
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`--max-keys <maxKeys:number>\` - Page size (default 100)
|
||||
- \`--marker <marker:string>\` - Pagination marker from a previous response
|
||||
- \`--storage <storage:string>\` - Secondary storage name (omit for the workspace default)
|
||||
- \`object-storage upload <local_path:string> <file_key:string>\` - Upload a local file to object storage at the given file key.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--content-type <contentType:string>\` - Content-Type header to set on the object
|
||||
- \`--content-disposition <contentDisposition:string>\` - Content-Disposition header to set on the object
|
||||
- \`object-storage download <file_key:string> [output_path:string]\` - Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--stdout\` - Write file contents to stdout instead of a file
|
||||
- \`object-storage delete <file_key:string>\` - Delete an object from object storage. Prompts for confirmation unless --yes is set.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--yes\` - Skip the confirmation prompt
|
||||
- \`object-storage move <src_file_key:string> <dest_file_key:string>\` - Move an object within the same storage (rename or relocate by key).
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`object-storage info <file_key:string>\` - Show metadata (size, mime, last-modified) for an object.
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`object-storage preview <file_key:string>\` - Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--mime <mime:string>\` - Override the detected mime type (e.g. text/csv)
|
||||
- \`--bytes-from <bytesFrom:number>\` - Start offset in bytes
|
||||
- \`--bytes-length <bytesLength:number>\` - Number of bytes to read
|
||||
- \`--csv-separator <csvSeparator:string>\` - CSV column separator (default ,)
|
||||
- \`--csv-header\` - Treat the first CSV row as a header
|
||||
|
||||
### protection-rules
|
||||
|
||||
**Subcommands:**
|
||||
@@ -7393,6 +7440,26 @@ workspace related commands
|
||||
- \`--team-name <team_name:string>\` - Slack team name
|
||||
- \`workspace disconnect-slack\`
|
||||
|
||||
|
||||
|
||||
# Object Storage CLI
|
||||
|
||||
\`wmill object-storage\` (alias \`wmill s3\`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace \`/job_helpers/*\` endpoints.
|
||||
|
||||
## Key concepts (not obvious from per-command --help)
|
||||
|
||||
- **\`file_key\` is the path inside the bucket** (e.g. \`reports/2026-05/orders.csv\`), not a Windmill path. Do NOT pass \`u/...\` or \`f/...\` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket.
|
||||
- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target.
|
||||
- **\`--storage <name>\` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use \`wmill object-storage list\` to discover configured storages.
|
||||
- **\`preview\` vs \`download\`**: \`preview\` returns a peek (CSV first rows, text content, or a byte slice via \`--bytes-from\`/\`--bytes-length\`) without writing to disk. Use \`download\` when you want the full file on disk.
|
||||
|
||||
## Choosing a subcommand
|
||||
|
||||
- Look at what's there: \`wmill object-storage files [prefix]\` (alias \`ls\`) — paginated, use \`--marker\` to continue.
|
||||
- Inspect one file: \`wmill object-storage info <file_key>\` for size/mime/last-modified, \`wmill object-storage preview <file_key>\` for content peek.
|
||||
- Move data in: \`wmill object-storage upload <local_path> <file_key>\` — set \`--content-type\` if the receiver cares (e.g. \`text/csv\`).
|
||||
- Move data out: \`wmill object-storage download <file_key> [output_path]\` — \`--stdout\` to pipe.
|
||||
- Reorganize: \`wmill object-storage move <src> <dest>\` (same storage), \`wmill object-storage delete <file_key>\` (interactive confirm unless \`--yes\`).
|
||||
`,
|
||||
"preview": `---
|
||||
name: preview
|
||||
|
||||
@@ -52,6 +52,7 @@ import docs from "./commands/docs/docs.ts";
|
||||
import config from "./commands/config/config.ts";
|
||||
import datatable from "./commands/datatable/datatable.ts";
|
||||
import ducklake from "./commands/ducklake/ducklake.ts";
|
||||
import objectStorage from "./commands/object-storage/object-storage.ts";
|
||||
import { fetchVersion } from "./core/context.ts";
|
||||
|
||||
export {
|
||||
@@ -77,6 +78,7 @@ export {
|
||||
config,
|
||||
datatable,
|
||||
ducklake,
|
||||
objectStorage,
|
||||
hubPull,
|
||||
pull,
|
||||
push,
|
||||
@@ -210,6 +212,7 @@ const command = new Command()
|
||||
.command("config", config)
|
||||
.command("datatable", datatable)
|
||||
.command("ducklake", ducklake)
|
||||
.command("object-storage", objectStorage)
|
||||
.command("version --version", "Show version information")
|
||||
.action(async (opts: any) => {
|
||||
console.log("CLI version: " + VERSION);
|
||||
|
||||
@@ -152,6 +152,10 @@ flow related commands
|
||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
||||
- `--remote` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `flow test-step <flow_path:string> <step_id:string>` - Test a single step of a local flow in isolation. Resolves the step by id (including nested branchone/branchall/forloopflow/whileloopflow and the failure/preprocessor modules), runs only that step's runnable. Supported step types: rawscript, script (PathScript), flow (PathFlow).
|
||||
- `-d --data <data:string>` - Step inputs as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
||||
- `--json` - Output the result as JSON (same as --silent)
|
||||
- `flow new <flow_path:string>` - create a new empty flow
|
||||
- `--summary <summary:string>` - flow summary
|
||||
- `--description <description:string>` - flow description
|
||||
@@ -367,6 +371,42 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- `--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
|
||||
|
||||
### object-storage
|
||||
|
||||
**Alias:** `s3`
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- `object-storage list` - List configured object storages for the workspace (default + secondary).
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `object-storage files [prefix:string]` - List files in an object storage. Optionally filter by prefix.
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `--max-keys <maxKeys:number>` - Page size (default 100)
|
||||
- `--marker <marker:string>` - Pagination marker from a previous response
|
||||
- `--storage <storage:string>` - Secondary storage name (omit for the workspace default)
|
||||
- `object-storage upload <local_path:string> <file_key:string>` - Upload a local file to object storage at the given file key.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--content-type <contentType:string>` - Content-Type header to set on the object
|
||||
- `--content-disposition <contentDisposition:string>` - Content-Disposition header to set on the object
|
||||
- `object-storage download <file_key:string> [output_path:string]` - Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--stdout` - Write file contents to stdout instead of a file
|
||||
- `object-storage delete <file_key:string>` - Delete an object from object storage. Prompts for confirmation unless --yes is set.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--yes` - Skip the confirmation prompt
|
||||
- `object-storage move <src_file_key:string> <dest_file_key:string>` - Move an object within the same storage (rename or relocate by key).
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `object-storage info <file_key:string>` - Show metadata (size, mime, last-modified) for an object.
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `object-storage preview <file_key:string>` - Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--mime <mime:string>` - Override the detected mime type (e.g. text/csv)
|
||||
- `--bytes-from <bytesFrom:number>` - Start offset in bytes
|
||||
- `--bytes-length <bytesLength:number>` - Number of bytes to read
|
||||
- `--csv-separator <csvSeparator:string>` - CSV column separator (default ,)
|
||||
- `--csv-header` - Treat the first CSV row as a header
|
||||
|
||||
### protection-rules
|
||||
|
||||
**Subcommands:**
|
||||
@@ -703,3 +743,23 @@ workspace related commands
|
||||
- `--team-name <team_name:string>` - Slack team name
|
||||
- `workspace disconnect-slack`
|
||||
|
||||
|
||||
|
||||
# Object Storage CLI
|
||||
|
||||
`wmill object-storage` (alias `wmill s3`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace `/job_helpers/*` endpoints.
|
||||
|
||||
## Key concepts (not obvious from per-command --help)
|
||||
|
||||
- **`file_key` is the path inside the bucket** (e.g. `reports/2026-05/orders.csv`), not a Windmill path. Do NOT pass `u/...` or `f/...` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket.
|
||||
- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target.
|
||||
- **`--storage <name>` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use `wmill object-storage list` to discover configured storages.
|
||||
- **`preview` vs `download`**: `preview` returns a peek (CSV first rows, text content, or a byte slice via `--bytes-from`/`--bytes-length`) without writing to disk. Use `download` when you want the full file on disk.
|
||||
|
||||
## Choosing a subcommand
|
||||
|
||||
- Look at what's there: `wmill object-storage files [prefix]` (alias `ls`) — paginated, use `--marker` to continue.
|
||||
- Inspect one file: `wmill object-storage info <file_key>` for size/mime/last-modified, `wmill object-storage preview <file_key>` for content peek.
|
||||
- Move data in: `wmill object-storage upload <local_path> <file_key>` — set `--content-type` if the receiver cares (e.g. `text/csv`).
|
||||
- Move data out: `wmill object-storage download <file_key> [output_path]` — `--stdout` to pipe.
|
||||
- Reorganize: `wmill object-storage move <src> <dest>` (same storage), `wmill object-storage delete <file_key>` (interactive confirm unless `--yes`).
|
||||
|
||||
@@ -2702,6 +2702,10 @@ flow related commands
|
||||
- \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not output anything other then the final output. Useful for scripting.
|
||||
- \`--remote\` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- \`flow test-step <flow_path:string> <step_id:string>\` - Test a single step of a local flow in isolation. Resolves the step by id (including nested branchone/branchall/forloopflow/whileloopflow and the failure/preprocessor modules), runs only that step's runnable. Supported step types: rawscript, script (PathScript), flow (PathFlow).
|
||||
- \`-d --data <data:string>\` - Step inputs as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not output anything other then the final output. Useful for scripting.
|
||||
- \`--json\` - Output the result as JSON (same as --silent)
|
||||
- \`flow new <flow_path:string>\` - create a new empty flow
|
||||
- \`--summary <summary:string>\` - flow summary
|
||||
- \`--description <description:string>\` - flow description
|
||||
@@ -2917,6 +2921,42 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- \`--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
|
||||
|
||||
### object-storage
|
||||
|
||||
**Alias:** \`s3\`
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`object-storage list\` - List configured object storages for the workspace (default + secondary).
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`object-storage files [prefix:string]\` - List files in an object storage. Optionally filter by prefix.
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`--max-keys <maxKeys:number>\` - Page size (default 100)
|
||||
- \`--marker <marker:string>\` - Pagination marker from a previous response
|
||||
- \`--storage <storage:string>\` - Secondary storage name (omit for the workspace default)
|
||||
- \`object-storage upload <local_path:string> <file_key:string>\` - Upload a local file to object storage at the given file key.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--content-type <contentType:string>\` - Content-Type header to set on the object
|
||||
- \`--content-disposition <contentDisposition:string>\` - Content-Disposition header to set on the object
|
||||
- \`object-storage download <file_key:string> [output_path:string]\` - Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--stdout\` - Write file contents to stdout instead of a file
|
||||
- \`object-storage delete <file_key:string>\` - Delete an object from object storage. Prompts for confirmation unless --yes is set.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--yes\` - Skip the confirmation prompt
|
||||
- \`object-storage move <src_file_key:string> <dest_file_key:string>\` - Move an object within the same storage (rename or relocate by key).
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`object-storage info <file_key:string>\` - Show metadata (size, mime, last-modified) for an object.
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`object-storage preview <file_key:string>\` - Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--mime <mime:string>\` - Override the detected mime type (e.g. text/csv)
|
||||
- \`--bytes-from <bytesFrom:number>\` - Start offset in bytes
|
||||
- \`--bytes-length <bytesLength:number>\` - Number of bytes to read
|
||||
- \`--csv-separator <csvSeparator:string>\` - CSV column separator (default ,)
|
||||
- \`--csv-header\` - Treat the first CSV row as a header
|
||||
|
||||
### protection-rules
|
||||
|
||||
**Subcommands:**
|
||||
@@ -3253,6 +3293,26 @@ workspace related commands
|
||||
- \`--team-name <team_name:string>\` - Slack team name
|
||||
- \`workspace disconnect-slack\`
|
||||
|
||||
|
||||
|
||||
# Object Storage CLI
|
||||
|
||||
\`wmill object-storage\` (alias \`wmill s3\`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace \`/job_helpers/*\` endpoints.
|
||||
|
||||
## Key concepts (not obvious from per-command --help)
|
||||
|
||||
- **\`file_key\` is the path inside the bucket** (e.g. \`reports/2026-05/orders.csv\`), not a Windmill path. Do NOT pass \`u/...\` or \`f/...\` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket.
|
||||
- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target.
|
||||
- **\`--storage <name>\` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use \`wmill object-storage list\` to discover configured storages.
|
||||
- **\`preview\` vs \`download\`**: \`preview\` returns a peek (CSV first rows, text content, or a byte slice via \`--bytes-from\`/\`--bytes-length\`) without writing to disk. Use \`download\` when you want the full file on disk.
|
||||
|
||||
## Choosing a subcommand
|
||||
|
||||
- Look at what's there: \`wmill object-storage files [prefix]\` (alias \`ls\`) — paginated, use \`--marker\` to continue.
|
||||
- Inspect one file: \`wmill object-storage info <file_key>\` for size/mime/last-modified, \`wmill object-storage preview <file_key>\` for content peek.
|
||||
- Move data in: \`wmill object-storage upload <local_path> <file_key>\` — set \`--content-type\` if the receiver cares (e.g. \`text/csv\`).
|
||||
- Move data out: \`wmill object-storage download <file_key> [output_path]\` — \`--stdout\` to pipe.
|
||||
- Reorganize: \`wmill object-storage move <src> <dest>\` (same storage), \`wmill object-storage delete <file_key>\` (interactive confirm unless \`--yes\`).
|
||||
`;
|
||||
|
||||
export const LANG_BASH = `# Bash
|
||||
|
||||
@@ -157,6 +157,10 @@ flow related commands
|
||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
||||
- `--remote` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `flow test-step <flow_path:string> <step_id:string>` - Test a single step of a local flow in isolation. Resolves the step by id (including nested branchone/branchall/forloopflow/whileloopflow and the failure/preprocessor modules), runs only that step's runnable. Supported step types: rawscript, script (PathScript), flow (PathFlow).
|
||||
- `-d --data <data:string>` - Step inputs as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
||||
- `--json` - Output the result as JSON (same as --silent)
|
||||
- `flow new <flow_path:string>` - create a new empty flow
|
||||
- `--summary <summary:string>` - flow summary
|
||||
- `--description <description:string>` - flow description
|
||||
@@ -372,6 +376,42 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- `--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
|
||||
|
||||
### object-storage
|
||||
|
||||
**Alias:** `s3`
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- `object-storage list` - List configured object storages for the workspace (default + secondary).
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `object-storage files [prefix:string]` - List files in an object storage. Optionally filter by prefix.
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `--max-keys <maxKeys:number>` - Page size (default 100)
|
||||
- `--marker <marker:string>` - Pagination marker from a previous response
|
||||
- `--storage <storage:string>` - Secondary storage name (omit for the workspace default)
|
||||
- `object-storage upload <local_path:string> <file_key:string>` - Upload a local file to object storage at the given file key.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--content-type <contentType:string>` - Content-Type header to set on the object
|
||||
- `--content-disposition <contentDisposition:string>` - Content-Disposition header to set on the object
|
||||
- `object-storage download <file_key:string> [output_path:string]` - Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--stdout` - Write file contents to stdout instead of a file
|
||||
- `object-storage delete <file_key:string>` - Delete an object from object storage. Prompts for confirmation unless --yes is set.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--yes` - Skip the confirmation prompt
|
||||
- `object-storage move <src_file_key:string> <dest_file_key:string>` - Move an object within the same storage (rename or relocate by key).
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `object-storage info <file_key:string>` - Show metadata (size, mime, last-modified) for an object.
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `object-storage preview <file_key:string>` - Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--mime <mime:string>` - Override the detected mime type (e.g. text/csv)
|
||||
- `--bytes-from <bytesFrom:number>` - Start offset in bytes
|
||||
- `--bytes-length <bytesLength:number>` - Number of bytes to read
|
||||
- `--csv-separator <csvSeparator:string>` - CSV column separator (default ,)
|
||||
- `--csv-header` - Treat the first CSV row as a header
|
||||
|
||||
### protection-rules
|
||||
|
||||
**Subcommands:**
|
||||
@@ -708,3 +748,23 @@ workspace related commands
|
||||
- `--team-name <team_name:string>` - Slack team name
|
||||
- `workspace disconnect-slack`
|
||||
|
||||
|
||||
|
||||
# Object Storage CLI
|
||||
|
||||
`wmill object-storage` (alias `wmill s3`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace `/job_helpers/*` endpoints.
|
||||
|
||||
## Key concepts (not obvious from per-command --help)
|
||||
|
||||
- **`file_key` is the path inside the bucket** (e.g. `reports/2026-05/orders.csv`), not a Windmill path. Do NOT pass `u/...` or `f/...` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket.
|
||||
- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target.
|
||||
- **`--storage <name>` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use `wmill object-storage list` to discover configured storages.
|
||||
- **`preview` vs `download`**: `preview` returns a peek (CSV first rows, text content, or a byte slice via `--bytes-from`/`--bytes-length`) without writing to disk. Use `download` when you want the full file on disk.
|
||||
|
||||
## Choosing a subcommand
|
||||
|
||||
- Look at what's there: `wmill object-storage files [prefix]` (alias `ls`) — paginated, use `--marker` to continue.
|
||||
- Inspect one file: `wmill object-storage info <file_key>` for size/mime/last-modified, `wmill object-storage preview <file_key>` for content peek.
|
||||
- Move data in: `wmill object-storage upload <local_path> <file_key>` — set `--content-type` if the receiver cares (e.g. `text/csv`).
|
||||
- Move data out: `wmill object-storage download <file_key> [output_path]` — `--stdout` to pipe.
|
||||
- Reorganize: `wmill object-storage move <src> <dest>` (same storage), `wmill object-storage delete <file_key>` (interactive confirm unless `--yes`).
|
||||
|
||||
@@ -47,6 +47,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `wmill flow test-step <flow_path> <step_id>` — runs a single step of the local flow in isolation. Use when iterating on one module (rawscript / script / flow types) and you don't want to wait for upstream steps. Supports nested steps inside branchone/branchall/forloopflow/whileloopflow, plus the special `preprocessor` and `failure` modules by id. Pass step args with `-d '<json>'`.
|
||||
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — regenerate stale `.lock` and `.script.yaml` files. By default it scans **scripts, flows, and apps** across the workspace; pass `--skip-flows --skip-apps` (or run from a subdirectory) to limit the scope when you only care about the flow you edited.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
@@ -63,6 +64,12 @@ Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### Test a single step vs preview the whole flow
|
||||
|
||||
Use `flow test-step <flow_path> <step_id>` when the user is iterating on one module and the flow's upstream steps aren't part of what they're trying to validate. It runs only that step's runnable (rawscript: the inline script; script: the PathScript fetched from the workspace; flow: the subflow by path) and is much faster than running the whole flow when previous steps are slow or expensive.
|
||||
|
||||
Use `flow preview <flow_path>` when steps depend on each other's outputs, when the user is validating the overall control flow, or when `test-step` doesn't apply (branchone, branchall, forloopflow, whileloopflow, identity, and AI agent steps cannot themselves be tested in isolation — for branchone/branchall/forloopflow/whileloopflow, the *contained* steps can, by passing the inner step's id).
|
||||
|
||||
### After writing — offer to run, don't wait passively
|
||||
|
||||
This is about **programmatic execution** (`wmill flow preview -d '<args>'`), which actually runs the flow and has side effects. Visual preview (the `preview` skill) is offered separately — see "Visual preview" below.
|
||||
|
||||
@@ -42,6 +42,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `wmill flow test-step <flow_path> <step_id>` — runs a single step of the local flow in isolation. Use when iterating on one module (rawscript / script / flow types) and you don't want to wait for upstream steps. Supports nested steps inside branchone/branchall/forloopflow/whileloopflow, plus the special `preprocessor` and `failure` modules by id. Pass step args with `-d '<json>'`.
|
||||
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — regenerate stale `.lock` and `.script.yaml` files. By default it scans **scripts, flows, and apps** across the workspace; pass `--skip-flows --skip-apps` (or run from a subdirectory) to limit the scope when you only care about the flow you edited.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
@@ -58,6 +59,12 @@ Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### Test a single step vs preview the whole flow
|
||||
|
||||
Use `flow test-step <flow_path> <step_id>` when the user is iterating on one module and the flow's upstream steps aren't part of what they're trying to validate. It runs only that step's runnable (rawscript: the inline script; script: the PathScript fetched from the workspace; flow: the subflow by path) and is much faster than running the whole flow when previous steps are slow or expensive.
|
||||
|
||||
Use `flow preview <flow_path>` when steps depend on each other's outputs, when the user is validating the overall control flow, or when `test-step` doesn't apply (branchone, branchall, forloopflow, whileloopflow, identity, and AI agent steps cannot themselves be tested in isolation — for branchone/branchall/forloopflow/whileloopflow, the *contained* steps can, by passing the inner step's id).
|
||||
|
||||
### After writing — offer to run, don't wait passively
|
||||
|
||||
This is about **programmatic execution** (`wmill flow preview -d '<args>'`), which actually runs the flow and has side effects. Visual preview (the `preview` skill) is offered separately — see "Visual preview" below.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Object Storage CLI
|
||||
|
||||
`wmill object-storage` (alias `wmill s3`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace `/job_helpers/*` endpoints.
|
||||
|
||||
## Key concepts (not obvious from per-command --help)
|
||||
|
||||
- **`file_key` is the path inside the bucket** (e.g. `reports/2026-05/orders.csv`), not a Windmill path. Do NOT pass `u/...` or `f/...` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket.
|
||||
- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target.
|
||||
- **`--storage <name>` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use `wmill object-storage list` to discover configured storages.
|
||||
- **`preview` vs `download`**: `preview` returns a peek (CSV first rows, text content, or a byte slice via `--bytes-from`/`--bytes-length`) without writing to disk. Use `download` when you want the full file on disk.
|
||||
|
||||
## Choosing a subcommand
|
||||
|
||||
- Look at what's there: `wmill object-storage files [prefix]` (alias `ls`) — paginated, use `--marker` to continue.
|
||||
- Inspect one file: `wmill object-storage info <file_key>` for size/mime/last-modified, `wmill object-storage preview <file_key>` for content peek.
|
||||
- Move data in: `wmill object-storage upload <local_path> <file_key>` — set `--content-type` if the receiver cares (e.g. `text/csv`).
|
||||
- Move data out: `wmill object-storage download <file_key> [output_path]` — `--stdout` to pipe.
|
||||
- Reorganize: `wmill object-storage move <src> <dest>` (same storage), `wmill object-storage delete <file_key>` (interactive confirm unless `--yes`).
|
||||
@@ -2312,6 +2312,13 @@ def main():
|
||||
print("Extracting CLI commands...")
|
||||
cli_data = extract_cli_commands()
|
||||
cli_commands = generate_cli_commands_markdown(cli_data)
|
||||
# Append hand-written CLI guidance covering bits that aren't obvious from
|
||||
# the auto-generated per-command --help (file_key semantics, --storage,
|
||||
# workspace scope). The cli-commands skill is the entry point agents read
|
||||
# to learn about `wmill`, so non-obvious usage notes belong here.
|
||||
object_storage_cli = read_markdown_file(base_dir / "object-storage-cli.md")
|
||||
if object_storage_cli:
|
||||
cli_commands = f"{cli_commands}\n\n{object_storage_cli}"
|
||||
OUTPUT_CLI_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(OUTPUT_CLI_DIR / "cli-commands.md").write_text(cli_commands)
|
||||
print(f" Found {len(cli_data['commands'])} commands, {len(cli_data['global_options'])} global options")
|
||||
|
||||
Reference in New Issue
Block a user