mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 00:02:23 +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>
283 lines
7.8 KiB
TypeScript
283 lines
7.8 KiB
TypeScript
import { requireLogin } from "../../core/auth.ts";
|
|
import { resolveWorkspace, validatePath } from "../../core/context.ts";
|
|
import { Command } from "@cliffy/command";
|
|
import { Table } from "@cliffy/table";
|
|
import { colors } from "@cliffy/ansi/colors";
|
|
import * as log from "../../core/log.ts";
|
|
import { sep as SEP } from "node:path";
|
|
import * as windmillUtils from "@windmill-labs/shared-utils";
|
|
import { yamlParseFile } from "../../utils/yaml.ts";
|
|
import * as wmill from "../../../gen/services.gen.ts";
|
|
import { ListableApp, Policy } from "../../../gen/types.gen.ts";
|
|
|
|
import { GlobalOptions, isSuperset } from "../../types.ts";
|
|
import { readInlinePathSync } from "../../utils/utils.ts";
|
|
import devCommand from "./dev.ts";
|
|
import lintCommand from "./lint.ts";
|
|
import newCommand from "./new.ts";
|
|
import generateAgentsCommand from "./generate_agents.ts";
|
|
import { isVersionsGeq1585 } from "../sync/global.ts";
|
|
|
|
export interface AppFile {
|
|
value: any;
|
|
public?: boolean;
|
|
summary: string;
|
|
policy: Policy;
|
|
}
|
|
|
|
const alreadySynced: string[] = [];
|
|
|
|
function respecializeFields(fields: Record<string, any>) {
|
|
Object.entries(fields).forEach(([k, v]) => {
|
|
if (typeof v == "object") {
|
|
if (v.value !== undefined) {
|
|
fields[k] = { value: v.value, type: "static" };
|
|
} else if (v.ctx !== undefined) {
|
|
fields[k] = { ctx: v.ctx, type: "ctx" };
|
|
} else if (v.expr !== undefined) {
|
|
fields[k] = {
|
|
expr: v.expr,
|
|
allowUserResources: v.allowUserResources,
|
|
type: "javascript",
|
|
};
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
export function repopulateFields(runnables: Record<string, any>) {
|
|
Object.values(runnables).forEach((v) => {
|
|
if (typeof v == "object") {
|
|
if (v.fields !== undefined) {
|
|
respecializeFields(v.fields);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
export function replaceInlineScripts(
|
|
rec: any,
|
|
localPath: string,
|
|
addType: boolean
|
|
) {
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
if (typeof rec == "object") {
|
|
return Object.entries(rec).flatMap(([k, v]) => {
|
|
if (k == "runType") {
|
|
if (addType) {
|
|
if (isVersionsGeq1585()) {
|
|
rec["type"] = "path";
|
|
} else {
|
|
rec["type"] = "runnableByPath";
|
|
}
|
|
}
|
|
} else if (k == "inlineScript" && typeof v == "object") {
|
|
if (addType) {
|
|
if (isVersionsGeq1585()) {
|
|
rec["type"] = "inline";
|
|
} else {
|
|
rec["type"] = "runnableByName";
|
|
}
|
|
}
|
|
const o: Record<string, any> = v as any;
|
|
|
|
if (o["content"] && o["content"].startsWith("!inline")) {
|
|
const basePath = localPath + o["content"].split(" ")[1];
|
|
o["content"] = readInlinePathSync(basePath);
|
|
}
|
|
if (o["lock"] && o["lock"].startsWith("!inline")) {
|
|
const basePath = localPath + o["lock"].split(" ")[1];
|
|
o["lock"] = readInlinePathSync(basePath);
|
|
}
|
|
} else {
|
|
replaceInlineScripts(v, localPath, addType);
|
|
}
|
|
});
|
|
}
|
|
return [];
|
|
}
|
|
export function isExecutionModeAnonymous(app: any) {
|
|
return app?.["policy"]?.["execution_mode"] == "anonymous";
|
|
}
|
|
export async function pushApp(
|
|
workspace: string,
|
|
remotePath: string,
|
|
localPath: string,
|
|
message?: string
|
|
): Promise<void> {
|
|
if (alreadySynced.includes(localPath)) {
|
|
return;
|
|
}
|
|
alreadySynced.push(localPath);
|
|
remotePath = remotePath.replaceAll(SEP, "/");
|
|
let app: any = undefined;
|
|
// deleting old app if it exists in raw mode
|
|
try {
|
|
app = await wmill.getAppByPath({
|
|
workspace,
|
|
path: remotePath,
|
|
});
|
|
} catch {
|
|
//ignore
|
|
}
|
|
if (isExecutionModeAnonymous(app)) {
|
|
app.public = true;
|
|
}
|
|
// console.log(app);
|
|
if (app) {
|
|
app.policy = undefined;
|
|
}
|
|
|
|
if (!localPath.endsWith(SEP)) {
|
|
localPath += SEP;
|
|
}
|
|
const path = localPath + "app.yaml";
|
|
const localApp = (await yamlParseFile(path)) as AppFile;
|
|
|
|
replaceInlineScripts(localApp.value, localPath, true);
|
|
await generatingPolicy(
|
|
localApp,
|
|
remotePath,
|
|
localApp?.["public"] ??
|
|
localApp?.["policy"]?.["execution_mode"] == "anonymous"
|
|
);
|
|
if (app) {
|
|
if (isSuperset(localApp, app)) {
|
|
log.info(colors.green(`App ${remotePath} is up to date`));
|
|
return;
|
|
}
|
|
log.info(colors.bold.yellow(`Updating app ${remotePath}...`));
|
|
await wmill.updateApp({
|
|
workspace,
|
|
path: remotePath,
|
|
requestBody: {
|
|
deployment_message: message,
|
|
...localApp,
|
|
},
|
|
});
|
|
} else {
|
|
log.info(colors.yellow.bold("Creating new app..."));
|
|
|
|
await wmill.createApp({
|
|
workspace,
|
|
requestBody: {
|
|
path: remotePath,
|
|
deployment_message: message,
|
|
...localApp,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function generatingPolicy(
|
|
app: any,
|
|
path: string,
|
|
publicApp: boolean
|
|
) {
|
|
log.info(colors.gray(`Generating fresh policy for app ${path}...`));
|
|
try {
|
|
app.policy = await windmillUtils.updatePolicy(app.value, undefined);
|
|
app.policy.execution_mode = publicApp ? "anonymous" : "publisher";
|
|
} catch (e) {
|
|
log.error(colors.red(`Error generating policy for app ${path}: ${e}`));
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function list(opts: GlobalOptions & { includeDraftOnly?: boolean; json?: boolean }) {
|
|
const workspace = await resolveWorkspace(opts);
|
|
await requireLogin(opts);
|
|
|
|
let page = 0;
|
|
const perPage = 10;
|
|
const total: ListableApp[] = [];
|
|
while (true) {
|
|
const res = await wmill.listApps({
|
|
workspace: workspace.workspaceId,
|
|
page,
|
|
perPage,
|
|
includeDraftOnly: opts.includeDraftOnly ?? false,
|
|
});
|
|
page += 1;
|
|
total.push(...res);
|
|
if (res.length < perPage) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (opts.json) {
|
|
console.log(JSON.stringify(total));
|
|
} else {
|
|
new Table()
|
|
.header(["path", "summary"])
|
|
.padding(2)
|
|
.border(true)
|
|
.body(total.map((x) => [x.path, x.summary]))
|
|
.render();
|
|
}
|
|
}
|
|
|
|
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
|
const workspace = await resolveWorkspace(opts);
|
|
await requireLogin(opts);
|
|
const a = await wmill.getAppByPath({
|
|
workspace: workspace.workspaceId,
|
|
path,
|
|
});
|
|
if (opts.json) {
|
|
console.log(JSON.stringify(a));
|
|
} else {
|
|
console.log(colors.bold("Path:") + " " + a.path);
|
|
console.log(colors.bold("Summary:") + " " + (a.summary ?? ""));
|
|
console.log(colors.bold("Created by:") + " " + (a.created_by ?? ""));
|
|
}
|
|
}
|
|
|
|
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
|
if (!validatePath(remotePath)) {
|
|
return;
|
|
}
|
|
const workspace = await resolveWorkspace(opts);
|
|
await requireLogin(opts);
|
|
|
|
await pushApp(workspace.workspaceId, remotePath, filePath);
|
|
log.info(colors.bold.underline.green("App pushed"));
|
|
}
|
|
|
|
const command = new Command()
|
|
.description("app related commands")
|
|
.option("--json", "Output as JSON (for piping to jq)")
|
|
.action(list as any)
|
|
.command("list", "list all apps")
|
|
.option("--json", "Output as JSON (for piping to jq)")
|
|
.action(list as any)
|
|
.command("get", "get an app's details")
|
|
.arguments("<path:string>")
|
|
.option("--json", "Output as JSON (for piping to jq)")
|
|
.action(get as any)
|
|
.command("push", "push a local app ")
|
|
.arguments("<file_path:string> <remote_path:string>")
|
|
.action(push as any)
|
|
.command("dev", devCommand)
|
|
.command("lint", lintCommand)
|
|
.command("new", newCommand)
|
|
.command("generate-agents", generateAgentsCommand)
|
|
.command(
|
|
"generate-locks",
|
|
"re-generate the lockfiles for app runnables inline scripts that have changed"
|
|
)
|
|
.arguments("[app_folder:string]")
|
|
.option("--yes", "Skip confirmation prompt")
|
|
.option("--dry-run", "Perform a dry run without making changes")
|
|
.option(
|
|
"--default-ts <runtime:string>",
|
|
"Default TypeScript runtime (bun or deno)"
|
|
)
|
|
.action(async (opts: any, appFolder: string | undefined) => {
|
|
const { generateLocksCommand } = await import("./app_metadata.ts");
|
|
await generateLocksCommand(opts, appFolder);
|
|
});
|
|
|
|
export default command;
|