feat(cli): add consistent get/list/new subcommands for all item types (#8047)

* 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>
This commit is contained in:
Ruben Fiszel
2026-02-22 08:53:28 +01:00
committed by GitHub
parent a91c532eca
commit 4fedfdfd11
80 changed files with 3465 additions and 419 deletions

View File

@@ -1,5 +1,5 @@
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import * as log from "./log.ts";
import { setClient } from "./client.ts";
import * as wmill from "../../gen/services.gen.ts";
import { GlobalUserInfo } from "../../gen/types.gen.ts";

View File

@@ -1,4 +1,4 @@
import * as log from "@std/log";
import * as log from "./log.ts";
import { readFile, writeFile } from "node:fs/promises";
import { getStore } from "./store.ts";

View File

@@ -1,7 +1,7 @@
import * as log from "@std/log";
import * as log from "./log.ts";
import { yamlParseFile } from "../utils/yaml.ts";
import { Confirm } from "@cliffy/prompt/confirm";
import { stringify as yamlStringify } from "@std/yaml";
import { stringify as yamlStringify } from "yaml";
import {
getCurrentGitBranch,
getOriginalBranchForWorkspaceForks,
@@ -196,7 +196,7 @@ export async function readConfigFile(): Promise<SyncOptions> {
if (!wmillYamlPath) {
log.warn(
"No wmill.yaml found. Use 'wmill init' to bootstrap it. Using 'bun' as default typescript runtime."
"No wmill.yaml found. Use 'wmill init' to bootstrap it."
);
return {};
}

View File

@@ -1,5 +1,5 @@
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import * as log from "./log.ts";
import { Select } from "@cliffy/prompt/select";
import { Confirm } from "@cliffy/prompt/confirm";
import { Input } from "@cliffy/prompt/input";
@@ -459,11 +459,12 @@ export async function resolveWorkspace(
// forked workspace, that we detect through the branch name (only when not using branchOverride)
const res = await tryResolveWorkspace(opts);
if (!res.isError) {
const workspace = (res as { isError: false; value: Workspace }).value;
if (branchOverride || !branch || !branch.startsWith(WM_FORK_PREFIX)) {
return res.value;
return workspace;
} else {
log.info(
`Found an active workspace \`${res.value.name}\` but the branch name indicates this is a forked workspace. Ignoring active workspace and trying to resolve the correct workspace from the branch name \`${branch}\``
`Found an active workspace \`${workspace.name}\` but the branch name indicates this is a forked workspace. Ignoring active workspace and trying to resolve the correct workspace from the branch name \`${branch}\``
);
}
}
@@ -486,13 +487,41 @@ export async function resolveWorkspace(
}
}
// Fall back to active workspace (lowest priority)
// Fall back to active workspace
const activeWorkspace = await getActiveWorkspace(opts);
if (activeWorkspace) {
(opts as any).__secret_workspace = activeWorkspace;
return activeWorkspace;
}
// Last resort: auto-configure from Windmill environment variables
// (set by the worker for bash/script execution)
const envWorkspace = process.env["WM_WORKSPACE"];
const envToken = process.env["WM_TOKEN"];
const envBaseUrl =
process.env["BASE_INTERNAL_URL"] ?? process.env["BASE_URL"];
if (envWorkspace && envToken && envBaseUrl) {
let normalizedBaseUrl: string;
try {
normalizedBaseUrl = new URL(envBaseUrl).toString();
} catch {
log.info(colors.red(`Invalid BASE_INTERNAL_URL: ${envBaseUrl}`));
return process.exit(-1);
}
log.debug(
`Using workspace from environment variables: ${envWorkspace} on ${normalizedBaseUrl}`
);
const ws: Workspace = {
name: envWorkspace,
workspaceId: envWorkspace,
remote: normalizedBaseUrl,
token: envToken,
};
(opts as any).__secret_workspace = ws;
return ws;
}
// If everything failed, show error
log.info(colors.red.bold("No workspace given and no default set."));
return process.exit(-1);
@@ -532,7 +561,8 @@ export async function tryResolveVersion(
const workspaceRes = await tryResolveWorkspace(opts);
if (workspaceRes.isError) return undefined;
const version = await fetchVersion(workspaceRes.value.remote);
const workspace = (workspaceRes as { isError: false; value: Workspace }).value;
const version = await fetchVersion(workspace.remote);
try {
return Number.parseInt(

24
cli/src/core/log.ts Normal file
View File

@@ -0,0 +1,24 @@
let logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" = "INFO";
const levels = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
export function setup(level: "DEBUG" | "INFO" | "WARN" | "ERROR") {
logLevel = level;
}
export function debug(msg: unknown) {
if (levels[logLevel] <= levels.DEBUG)
console.log(`\x1b[90m${String(msg)}\x1b[39m`);
}
export function info(msg: unknown) {
console.log(`\x1b[34m${String(msg)}\x1b[39m`);
}
export function warn(msg: unknown) {
console.log(`\x1b[33m${String(msg)}\x1b[39m`);
}
export function error(msg: unknown) {
console.log(`\x1b[31m${String(msg)}\x1b[39m`);
}

View File

@@ -1,7 +1,7 @@
import { GlobalOptions } from "../types.ts";
import { colors } from "@cliffy/ansi/colors";
import * as getPort from "get-port";
import * as log from "@std/log";
import * as log from "./log.ts";
import * as open from "open";
import { Secret } from "@cliffy/prompt/secret";
import { Select } from "@cliffy/prompt/select";

View File

@@ -2,9 +2,9 @@ import process from "node:process";
import { writeFile } from "node:fs/promises";
import { colors } from "@cliffy/ansi/colors";
import { Confirm } from "@cliffy/prompt/confirm";
import * as log from "@std/log";
import * as log from "./log.ts";
import { yamlParseFile } from "../utils/yaml.ts";
import { stringify as yamlStringify } from "@std/yaml";
import { stringify as yamlStringify } from "yaml";
import * as wmill from "../../gen/services.gen.ts";
import { AIConfig, Config, GlobalSetting } from "../../gen/types.gen.ts";
import { compareInstanceObjects, InstanceSyncOptions } from "../commands/instance/instance.ts";