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:
hugocasa
2026-05-26 10:43:43 +00:00
committed by GitHub
parent a6c51b146d
commit 36f574ff95
11 changed files with 832 additions and 2 deletions
+199 -2
View File
@@ -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;
+67
View File
@@ -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
+3
View File
@@ -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`).
+60
View File
@@ -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.
+7
View File
@@ -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.
+18
View File
@@ -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`).
+7
View File
@@ -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")