mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 08:01:25 +00:00
4fedfdfd11
* feat(cli): add consistent get/list/new subcommands for all item types Make the CLI consistent so every item type (script, flow, app, resource, resource-type, variable, schedule, folder, trigger) supports get/list/new subcommands, enabling the CLI to be used as a full API client in bash scripts with jq piping. - Add --json flag to all list commands for machine-readable output - Register explicit "list" subcommand alongside default action - Add "get <path> [--json]" subcommand to fetch single items from API - Rename "bootstrap" to "new" for script/flow, keep "bootstrap" as alias - Add "new" subcommand for resource, resource-type, variable, schedule, folder, and trigger to create local template YAML files - Update cli-commands skill documentation for wmill init - Add integration tests for all new commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all * feat: install wmill CLI in Docker images and use it for bash variable/resource access - Install windmill-cli via bun in all Dockerfiles that include bun - DockerfileCli: switch from node:slim to oven/bun:slim - CLI: auto-configure from WM_WORKSPACE/WM_TOKEN/BASE_INTERNAL_URL env vars as last-resort fallback when no workspace is configured - Frontend: replace curl-based bash snippets with wmill variable/resource get - Add backend integration tests for wmill CLI in bash scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): install windmill-cli in backend test workflow Ensures wmill is available on PATH for bash integration tests that use `wmill variable get` and `wmill resource get`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(cli): replace @std/* Deno dependencies with Node.js equivalents Replace @std/log with a lightweight custom logger (core/log.ts), @std/path with node:path, and @std/yaml with the yaml npm package. Also fix process hang on exit, add --node option to install_dev.sh, and add missing hasRequiredPermissions to NpmProvider. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all * all * all * refactor(cli): replace @ayonli/jsext and @std/encoding with lightweight alternatives Replace @ayonli/jsext (8.4MB) with tar-stream (32kB) for tar creation, replace @std/encoding with Node.js Buffer.toString("hex"), and fix @windmill-labs/shared-utils to use direct npm instead of JSR mirror. Also resolve merge conflicts in sync.ts and fix pre-existing type errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(cli): use singleQuote YAML output and pass yamlOptions in gitsync pull The yaml library defaults to double quotes, but the codebase (and tests) expect single-quoted strings. Add singleQuote: true to yamlOptions and pass yamlOptions to gitsync-settings pull writeFile calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all * all * fix(cli): address code review feedback - Install CLI from source in backend tests instead of npm - Fix script bootstrap catch block to re-throw "File already exists" - Add type-safe local variable after trigger kind validation - Use created_by instead of policy.on_behalf_of for app get output - Note --kind is recommended for faster trigger lookup in help text - Document node symlink purpose in Dockerfiles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): use /usr/bin for wmill wrapper to ensure it's in PATH Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): install wmill to ~/.local/bin to avoid permission issues Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci(backend): switch to Blacksmith runner and add cargo caching - Switch from ubicloud-standard-16 to blacksmith-16vcpu-ubuntu-2404 for faster NVMe-backed builds - Add stickydisk for cargo target directory (persistent NVMe cache across runs) - Add cache for cargo registry and git dependencies - Upgrade DuckDB FFI cache from actions/cache@v3 to useblacksmith/cache@v1 - Enable CARGO_INCREMENTAL=1 to benefit from persistent target cache Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix ci --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
312 lines
9.1 KiB
TypeScript
312 lines
9.1 KiB
TypeScript
/**
|
|
* Type alias for enum values - can be an array of strings or undefined
|
|
*/
|
|
export type EnumType =
|
|
| string[]
|
|
| { label: string; value: string }[]
|
|
| undefined;
|
|
|
|
/**
|
|
* Represents a property in a JSON schema with various validation and display options
|
|
*/
|
|
export interface SchemaProperty {
|
|
type: string | undefined;
|
|
description?: string;
|
|
pattern?: string;
|
|
default?: any;
|
|
enum?: EnumType;
|
|
contentEncoding?: "base64" | "binary";
|
|
format?: string;
|
|
items?: {
|
|
type?: "string" | "number" | "bytes" | "object" | "resource";
|
|
contentEncoding?: "base64";
|
|
enum?: EnumType;
|
|
resourceType?: string;
|
|
properties?: { [name: string]: SchemaProperty };
|
|
};
|
|
min?: number;
|
|
max?: number;
|
|
currency?: string;
|
|
currencyLocale?: string;
|
|
multiselect?: boolean;
|
|
customErrorMessage?: string;
|
|
properties?: { [name: string]: SchemaProperty };
|
|
required?: string[];
|
|
showExpr?: string;
|
|
password?: boolean;
|
|
order?: string[];
|
|
nullable?: boolean;
|
|
dateFormat?: string;
|
|
title?: string;
|
|
placeholder?: string;
|
|
oneOf?: SchemaProperty[];
|
|
originalType?: string;
|
|
}
|
|
|
|
const ITEMS_PRESERVED_FIELDS = [
|
|
"properties",
|
|
"required",
|
|
"additionalProperties",
|
|
"enum",
|
|
"resourceType",
|
|
"contentEncoding",
|
|
"description",
|
|
] as (keyof SchemaProperty["items"])[];
|
|
|
|
/**
|
|
* Converts argument signature types to JSON schema properties.
|
|
* This function handles various Windmill-specific types and converts them
|
|
* to standard JSON schema format while preserving existing property metadata.
|
|
*
|
|
* @param t - The argument signature type definition (can be string or complex object types)
|
|
* @param oldS - Existing schema property to update with new type information
|
|
*/
|
|
|
|
export function argSigToJsonSchemaType(
|
|
t:
|
|
| string
|
|
| { resource: string | null }
|
|
| {
|
|
list:
|
|
| (string | { name?: string; props?: { key: string; typ: any }[] })
|
|
| { str: any }
|
|
| { object: { name?: string; props?: { key: string; typ: any }[] } }
|
|
| null;
|
|
}
|
|
| { dynselect: string }
|
|
| { dynmultiselect: string }
|
|
| { str: string[] | null }
|
|
| { object: { name?: string; props?: { key: string; typ: any }[] } }
|
|
| {
|
|
oneof: {
|
|
label: string;
|
|
properties: { key: string; typ: any }[];
|
|
}[];
|
|
},
|
|
oldS: SchemaProperty
|
|
): void {
|
|
const newS: SchemaProperty = { type: "" };
|
|
if (t === "int") {
|
|
newS.type = "integer";
|
|
} else if (t === "float") {
|
|
newS.type = "number";
|
|
} else if (t === "bool") {
|
|
newS.type = "boolean";
|
|
} else if (t === "email") {
|
|
newS.type = "string";
|
|
newS.format = "email";
|
|
} else if (t === "sql") {
|
|
newS.type = "string";
|
|
newS.format = "sql";
|
|
} else if (t === "yaml") {
|
|
newS.type = "string";
|
|
newS.format = "yaml";
|
|
} else if (t === "bytes") {
|
|
newS.type = "string";
|
|
newS.contentEncoding = "base64";
|
|
newS.originalType = "bytes";
|
|
} else if (t === "datetime") {
|
|
newS.type = "string";
|
|
newS.format = "date-time";
|
|
} else if (t === "date") {
|
|
newS.type = "string";
|
|
newS.format = "date";
|
|
} else if (typeof t !== "string" && "oneof" in t) {
|
|
newS.type = "object";
|
|
if (t.oneof) {
|
|
newS.oneOf = t.oneof.map((obj) => {
|
|
const oldObjS =
|
|
oldS.oneOf?.find((o) => o?.title === obj.label) ?? undefined;
|
|
const properties: Record<string, any> = {};
|
|
for (const prop of obj.properties) {
|
|
if (oldObjS?.properties && prop.key in oldObjS?.properties) {
|
|
properties[prop.key] = oldObjS?.properties[prop.key];
|
|
} else {
|
|
properties[prop.key] = { description: "", type: "" };
|
|
}
|
|
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
|
|
}
|
|
return {
|
|
type: "object",
|
|
title: obj.label,
|
|
properties,
|
|
order: oldObjS?.order ?? undefined,
|
|
};
|
|
});
|
|
}
|
|
} else if (typeof t !== "string" && `object` in t) {
|
|
newS.type = "object";
|
|
if (t.object.name) {
|
|
newS.format = `resource-${t.object.name}`;
|
|
}
|
|
if (t.object.props) {
|
|
const properties: Record<string, any> = {};
|
|
for (const prop of t.object.props) {
|
|
if (oldS.properties && prop.key in oldS.properties) {
|
|
properties[prop.key] = oldS.properties[prop.key];
|
|
} else {
|
|
properties[prop.key] = { description: "", type: "" };
|
|
}
|
|
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
|
|
}
|
|
newS.properties = properties;
|
|
}
|
|
} else if (typeof t !== "string" && `str` in t) {
|
|
newS.type = "string";
|
|
if (t.str) {
|
|
newS.originalType = "enum";
|
|
newS.enum = t.str;
|
|
} else if (oldS.originalType == "string" && oldS.enum) {
|
|
newS.originalType = "string";
|
|
newS.enum = oldS.enum;
|
|
} else {
|
|
newS.originalType = "string";
|
|
newS.enum = undefined;
|
|
}
|
|
} else if (typeof t !== "string" && `resource` in t) {
|
|
newS.type = "object";
|
|
newS.format = `resource-${t.resource}`;
|
|
} else if (typeof t !== "string" && `dynselect` in t) {
|
|
newS.type = "object";
|
|
newS.format = `dynselect-${t.dynselect}`;
|
|
} else if (typeof t !== "string" && `dynmultiselect` in t) {
|
|
newS.type = "object";
|
|
newS.format = `dynmultiselect-${t.dynmultiselect}`;
|
|
} else if (typeof t !== "string" && `list` in t) {
|
|
newS.type = "array";
|
|
if (t.list === "int" || t.list === "float") {
|
|
newS.items = { type: "number" };
|
|
newS.originalType = "number[]";
|
|
} else if (t.list === "bytes") {
|
|
newS.items = { type: "string", contentEncoding: "base64" };
|
|
newS.originalType = "bytes[]";
|
|
} else if (
|
|
t.list &&
|
|
typeof t.list == "object" &&
|
|
"str" in t.list &&
|
|
t.list.str
|
|
) {
|
|
newS.items = { type: "string", enum: t.list.str };
|
|
newS.originalType = "enum[]";
|
|
} else if (
|
|
t.list == "string" ||
|
|
(t.list && typeof t.list == "object" && "str" in t.list)
|
|
) {
|
|
newS.items = { type: "string", enum: oldS.items?.enum };
|
|
newS.originalType = "string[]";
|
|
} else if (
|
|
t.list &&
|
|
typeof t.list == "object" &&
|
|
"resource" in t.list &&
|
|
t.list.resource
|
|
) {
|
|
newS.items = {
|
|
type: "resource",
|
|
resourceType: t.list.resource as string,
|
|
};
|
|
newS.originalType = "resource[]";
|
|
} else if (
|
|
t.list &&
|
|
typeof t.list == "object" &&
|
|
"object" in t.list &&
|
|
t.list.object
|
|
) {
|
|
if (t.list.object.name) {
|
|
newS.format = `resource-${t.list.object.name}`;
|
|
}
|
|
if (t.list.object.props && t.list.object.props.length > 0) {
|
|
const properties: Record<string, any> = {};
|
|
for (const prop of t.list.object.props) {
|
|
properties[prop.key] = { description: "", type: "" };
|
|
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
|
|
}
|
|
newS.items = { type: "object", properties: properties };
|
|
} else {
|
|
// Preserve ALL user-defined fields when parser cannot infer structure
|
|
newS.items = { type: oldS.items?.type || "object" };
|
|
|
|
if (oldS.items && typeof oldS.items === "object") {
|
|
ITEMS_PRESERVED_FIELDS.forEach((field) => {
|
|
if (oldS.items && oldS.items[field] !== undefined) {
|
|
(newS.items as any)[field] = oldS.items[field];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
newS.originalType = "record[]";
|
|
} else {
|
|
// Preserve ALL user-defined fields for untyped lists (same as record[] branch)
|
|
newS.items = { type: oldS.items?.type || "object" };
|
|
|
|
if (oldS.items && typeof oldS.items === "object") {
|
|
ITEMS_PRESERVED_FIELDS.forEach((field) => {
|
|
if (oldS.items && oldS.items[field] !== undefined) {
|
|
(newS.items as any)[field] = oldS.items[field];
|
|
}
|
|
});
|
|
}
|
|
newS.originalType = "object[]";
|
|
}
|
|
} else {
|
|
// Preserve existing type when inference fails, default to "object" for undefined/null
|
|
newS.type = oldS.type ?? "object";
|
|
}
|
|
|
|
const preservedFields = [
|
|
"description",
|
|
"pattern",
|
|
"min",
|
|
"max",
|
|
"currency",
|
|
"currencyLocale",
|
|
"multiselect",
|
|
"customErrorMessage",
|
|
"required",
|
|
"showExpr",
|
|
"password",
|
|
"order",
|
|
"dateFormat",
|
|
"title",
|
|
"placeholder",
|
|
];
|
|
|
|
preservedFields.forEach((field) => {
|
|
// @ts-ignore
|
|
if (oldS[field] !== undefined) {
|
|
// @ts-ignore
|
|
newS[field] = oldS[field];
|
|
}
|
|
});
|
|
|
|
if (oldS.type != newS.type) {
|
|
for (const prop of Object.getOwnPropertyNames(newS)) {
|
|
if (prop != "description") {
|
|
// @ts-ignore
|
|
delete oldS[prop];
|
|
}
|
|
}
|
|
} else if (
|
|
(oldS.format == "date" || oldS.format === "date-time") &&
|
|
newS.format == "string"
|
|
) {
|
|
newS.format = oldS.format;
|
|
} else if (newS.format == "date-time" && oldS.format == "date") {
|
|
newS.format = "date";
|
|
} else if (newS.format == "date" || newS.format == "date-time") {
|
|
newS.format = oldS.format;
|
|
} else if (oldS.items?.type != newS.items?.type) {
|
|
delete oldS.items;
|
|
} else if (oldS.type == "string" && oldS.format != undefined) {
|
|
newS.format = oldS.format;
|
|
}
|
|
|
|
if (
|
|
(oldS.type != newS.type || newS.type != "string") &&
|
|
newS.format == undefined
|
|
) {
|
|
oldS.format = undefined;
|
|
}
|
|
|
|
Object.assign(oldS, newS);
|
|
}
|