mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 08:07:03 +00:00
feat(cli): add job, group, audit, token commands and schedule enable/disable (#8581)
* feat(cli): add job, group, audit, token commands and schedule enable/disable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(cli): regenerate system prompts after new commands Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): address PR review feedback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(cli): regenerate system prompts after review fixes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(cli): extract shared formatTimestamp util and remove unused resolveWorkspace in token Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
820f28f879
commit
d29cb234db
@@ -0,0 +1,113 @@
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace } 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 { mergeConfigWithConfigFile } from "../../core/conf.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { formatTimestamp } from "../../utils/utils.ts";
|
||||
|
||||
async function list(
|
||||
opts: GlobalOptions & {
|
||||
json?: boolean;
|
||||
username?: string;
|
||||
operation?: string;
|
||||
actionKind?: string;
|
||||
before?: string;
|
||||
after?: string;
|
||||
limit?: number;
|
||||
}
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const logs = await wmill.listAuditLogs({
|
||||
workspace: workspace.workspaceId,
|
||||
username: opts.username,
|
||||
operation: opts.operation,
|
||||
actionKind: opts.actionKind as any,
|
||||
before: opts.before,
|
||||
after: opts.after,
|
||||
perPage: opts.limit ?? 30,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(logs));
|
||||
} else {
|
||||
if (logs.length === 0) {
|
||||
log.info("No audit logs found.");
|
||||
return;
|
||||
}
|
||||
new Table()
|
||||
.header(["ID", "Timestamp", "Username", "Operation", "Action", "Resource"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(
|
||||
logs.map((l) => [
|
||||
String(l.id),
|
||||
formatTimestamp(l.timestamp),
|
||||
l.username,
|
||||
l.operation,
|
||||
l.action_kind,
|
||||
l.resource ?? "-",
|
||||
])
|
||||
)
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
async function get(
|
||||
opts: GlobalOptions & { json?: boolean },
|
||||
id: string
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const auditLog = await wmill.getAuditLog({
|
||||
workspace: workspace.workspaceId,
|
||||
id: parseInt(id, 10),
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(auditLog));
|
||||
} else {
|
||||
console.log(colors.bold("ID:") + " " + auditLog.id);
|
||||
console.log(colors.bold("Timestamp:") + " " + formatTimestamp(auditLog.timestamp));
|
||||
console.log(colors.bold("Username:") + " " + auditLog.username);
|
||||
console.log(colors.bold("Operation:") + " " + auditLog.operation);
|
||||
console.log(colors.bold("Action Kind:") + " " + auditLog.action_kind);
|
||||
console.log(colors.bold("Resource:") + " " + (auditLog.resource ?? "-"));
|
||||
if (auditLog.parameters && Object.keys(auditLog.parameters).length > 0) {
|
||||
console.log(colors.bold("Parameters:"));
|
||||
console.log(JSON.stringify(auditLog.parameters, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auditListOptions = (cmd: Command) =>
|
||||
cmd
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.option("--username <username:string>", "Filter by username")
|
||||
.option("--operation <operation:string>", "Filter by operation (exact or prefix)")
|
||||
.option("--action-kind <actionKind:string>", "Filter by action kind (Create, Update, Delete, Execute)")
|
||||
.option("--before <before:string>", "Filter events before this timestamp")
|
||||
.option("--after <after:string>", "Filter events after this timestamp")
|
||||
.option("--limit <limit:number>", "Number of entries to return (default 30, max 100)");
|
||||
|
||||
const command = auditListOptions(new Command()
|
||||
.description("View audit logs (requires admin)"))
|
||||
.action(list as any)
|
||||
.command("list", auditListOptions(new Command().description("List audit log entries")))
|
||||
.action(list as any)
|
||||
.command("get", "Get a specific audit log entry")
|
||||
.arguments("<id:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(get as any);
|
||||
|
||||
export default command;
|
||||
@@ -570,6 +570,71 @@ export async function bootstrap(
|
||||
writeFileSync(flowYamlPath, newFlowDefinitionYaml, { flag: "wx", encoding: "utf-8" });
|
||||
}
|
||||
|
||||
async function history(
|
||||
opts: GlobalOptions & { json?: boolean },
|
||||
flowPath: string
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const versions = await wmill.getFlowHistory({
|
||||
workspace: workspace.workspaceId,
|
||||
path: flowPath,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(versions));
|
||||
} else {
|
||||
if (versions.length === 0) {
|
||||
log.info("No version history found for " + flowPath);
|
||||
return;
|
||||
}
|
||||
new Table()
|
||||
.header(["Version", "Created At", "Deployment Message"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(
|
||||
versions.map((v) => [
|
||||
String(v.id),
|
||||
new Date(v.created_at).toISOString().replace("T", " ").substring(0, 19),
|
||||
v.deployment_msg ?? "-",
|
||||
])
|
||||
)
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
async function showVersion(
|
||||
opts: GlobalOptions & { json?: boolean },
|
||||
flowPath: string,
|
||||
version: string
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const flow = await wmill.getFlowVersion({
|
||||
workspace: workspace.workspaceId,
|
||||
path: flowPath,
|
||||
version: parseInt(version, 10),
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(flow));
|
||||
} else {
|
||||
console.log(colors.bold("Path:") + " " + flow.path);
|
||||
console.log(colors.bold("Summary:") + " " + (flow.summary ?? "-"));
|
||||
console.log(colors.bold("Description:") + " " + (flow.description ?? "-"));
|
||||
console.log(colors.bold("Schema:"));
|
||||
console.log(JSON.stringify(flow.schema, null, 2));
|
||||
console.log(colors.bold("Value:"));
|
||||
console.log(JSON.stringify(flow.value, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("flow related commands")
|
||||
.option("--show-archived", "Enable archived flows in output")
|
||||
@@ -643,6 +708,14 @@ const command = new Command()
|
||||
.arguments("<flow_path:string>")
|
||||
.option("--summary <summary:string>", "flow summary")
|
||||
.option("--description <description:string>", "flow description")
|
||||
.action(bootstrap as any);
|
||||
.action(bootstrap as any)
|
||||
.command("history", "Show version history for a flow")
|
||||
.arguments("<path:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(history as any)
|
||||
.command("show-version", "Show a specific version of a flow")
|
||||
.arguments("<path:string> <version:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(showVersion as any);
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace } 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 { mergeConfigWithConfigFile } from "../../core/conf.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
|
||||
async function list(opts: GlobalOptions & { json?: boolean }) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const groups = await wmill.listGroups({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(groups));
|
||||
} else {
|
||||
if (groups.length === 0) {
|
||||
log.info("No groups found.");
|
||||
return;
|
||||
}
|
||||
new Table()
|
||||
.header(["Name", "Summary", "Members"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(
|
||||
groups.map((g) => [
|
||||
g.name,
|
||||
g.summary ?? "-",
|
||||
String(g.members?.length ?? 0),
|
||||
])
|
||||
)
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
async function get(
|
||||
opts: GlobalOptions & { json?: boolean },
|
||||
name: string
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const group = await wmill.getGroup({
|
||||
workspace: workspace.workspaceId,
|
||||
name,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(group));
|
||||
} else {
|
||||
console.log(colors.bold("Name:") + " " + group.name);
|
||||
console.log(colors.bold("Summary:") + " " + (group.summary ?? "-"));
|
||||
console.log(
|
||||
colors.bold("Members:") +
|
||||
" " +
|
||||
(group.members && group.members.length > 0
|
||||
? group.members.join(", ")
|
||||
: "(none)")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function create(
|
||||
opts: GlobalOptions & { summary?: string },
|
||||
name: string
|
||||
) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await wmill.createGroup({
|
||||
workspace: workspace.workspaceId,
|
||||
requestBody: {
|
||||
name,
|
||||
summary: opts.summary,
|
||||
},
|
||||
});
|
||||
|
||||
log.info(colors.green(`Group '${name}' created.`));
|
||||
}
|
||||
|
||||
async function deleteGroup(opts: GlobalOptions, name: string) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await wmill.deleteGroup({
|
||||
workspace: workspace.workspaceId,
|
||||
name,
|
||||
});
|
||||
|
||||
log.info(colors.green(`Group '${name}' deleted.`));
|
||||
}
|
||||
|
||||
async function addUser(opts: GlobalOptions, name: string, username: string) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await wmill.addUserToGroup({
|
||||
workspace: workspace.workspaceId,
|
||||
name,
|
||||
requestBody: { username },
|
||||
});
|
||||
|
||||
log.info(colors.green(`User '${username}' added to group '${name}'.`));
|
||||
}
|
||||
|
||||
async function removeUser(opts: GlobalOptions, name: string, username: string) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await wmill.removeUserToGroup({
|
||||
workspace: workspace.workspaceId,
|
||||
name,
|
||||
requestBody: { username },
|
||||
});
|
||||
|
||||
log.info(colors.green(`User '${username}' removed from group '${name}'.`));
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("Manage workspace groups")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "List all groups in the workspace")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("get", "Get group details and members")
|
||||
.arguments("<name:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(get as any)
|
||||
.command("create", "Create a new group")
|
||||
.arguments("<name:string>")
|
||||
.option("--summary <summary:string>", "Group summary/description")
|
||||
.action(create as any)
|
||||
.command("delete", "Delete a group")
|
||||
.arguments("<name:string>")
|
||||
.action(deleteGroup as any)
|
||||
.command("add-user", "Add a user to a group")
|
||||
.arguments("<name:string> <username:string>")
|
||||
.action(addUser as any)
|
||||
.command("remove-user", "Remove a user from a group")
|
||||
.arguments("<name:string> <username:string>")
|
||||
.action(removeUser as any);
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,234 @@
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace } 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 { mergeConfigWithConfigFile } from "../../core/conf.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { formatTimestamp } from "../../utils/utils.ts";
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
if (minutes < 60) return `${minutes}m${remainingSeconds}s`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return `${hours}h${remainingMinutes}m`;
|
||||
}
|
||||
|
||||
function getJobStatus(job: any): string {
|
||||
if (job.type === "QueuedJob") {
|
||||
if (job.canceled) return colors.red("canceled");
|
||||
if (job.running) return colors.blue("running");
|
||||
return colors.yellow("queued");
|
||||
}
|
||||
// CompletedJob
|
||||
if (job.canceled) return colors.red("canceled");
|
||||
if (job.success) return colors.green("success");
|
||||
return colors.red("failure");
|
||||
}
|
||||
|
||||
function getJobStatusPlain(job: any): string {
|
||||
if (job.type === "QueuedJob") {
|
||||
if (job.canceled) return "canceled";
|
||||
if (job.running) return "running";
|
||||
return "queued";
|
||||
}
|
||||
if (job.canceled) return "canceled";
|
||||
if (job.success) return "success";
|
||||
return "failure";
|
||||
}
|
||||
|
||||
async function list(
|
||||
opts: GlobalOptions & {
|
||||
json?: boolean;
|
||||
scriptPath?: string;
|
||||
createdBy?: string;
|
||||
running?: boolean;
|
||||
success?: boolean;
|
||||
failed?: boolean;
|
||||
limit?: number;
|
||||
jobKinds?: string;
|
||||
label?: string;
|
||||
all?: boolean;
|
||||
}
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// --failed is a convenience alias for --success false
|
||||
let successFilter = opts.success;
|
||||
if (opts.failed) successFilter = false;
|
||||
|
||||
const jobs = await wmill.listJobs({
|
||||
workspace: workspace.workspaceId,
|
||||
scriptPathExact: opts.scriptPath,
|
||||
createdBy: opts.createdBy,
|
||||
running: opts.running,
|
||||
success: successFilter,
|
||||
perPage: Math.min(opts.limit ?? 30, 100),
|
||||
jobKinds: opts.jobKinds ?? "script,flow,singlestepflow",
|
||||
label: opts.label,
|
||||
hasNullParent: opts.all ? undefined : true,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(jobs));
|
||||
} else {
|
||||
if (jobs.length === 0) {
|
||||
log.info("No jobs found.");
|
||||
return;
|
||||
}
|
||||
new Table()
|
||||
.header(["ID", "Status", "Script/Flow", "Created By", "Duration", "Created At"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(
|
||||
jobs.map((j: any) => [
|
||||
j.id.substring(0, 8),
|
||||
getJobStatus(j),
|
||||
j.script_path ?? j.raw_code?.substring(0, 30) ?? "-",
|
||||
j.created_by ?? j.email ?? "-",
|
||||
j.duration_ms != null ? formatDuration(j.duration_ms) : (j.running ? "running" : "-"),
|
||||
j.created_at ? formatTimestamp(j.created_at) : "-",
|
||||
])
|
||||
)
|
||||
.render();
|
||||
log.info(`\nShowing ${jobs.length} job(s). Use --limit to show more.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function get(
|
||||
opts: GlobalOptions & { json?: boolean },
|
||||
id: string
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const job = await wmill.getJob({
|
||||
workspace: workspace.workspaceId,
|
||||
id,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(job));
|
||||
} else {
|
||||
const j = job as any;
|
||||
console.log(colors.bold("ID:") + " " + j.id);
|
||||
console.log(colors.bold("Status:") + " " + getJobStatusPlain(j));
|
||||
console.log(colors.bold("Kind:") + " " + j.job_kind);
|
||||
console.log(colors.bold("Script Path:") + " " + (j.script_path ?? "-"));
|
||||
console.log(colors.bold("Created By:") + " " + (j.created_by ?? "-"));
|
||||
console.log(colors.bold("Created At:") + " " + (j.created_at ? formatTimestamp(j.created_at) : "-"));
|
||||
if (j.started_at) {
|
||||
console.log(colors.bold("Started At:") + " " + formatTimestamp(j.started_at));
|
||||
}
|
||||
if (j.duration_ms != null) {
|
||||
console.log(colors.bold("Duration:") + " " + formatDuration(j.duration_ms));
|
||||
}
|
||||
if (j.schedule_path) {
|
||||
console.log(colors.bold("Schedule:") + " " + j.schedule_path);
|
||||
}
|
||||
if (j.result !== undefined) {
|
||||
console.log(colors.bold("Result:"));
|
||||
console.log(JSON.stringify(j.result, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function result(
|
||||
opts: GlobalOptions,
|
||||
id: string
|
||||
) {
|
||||
log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const jobResult = await wmill.getCompletedJobResult({
|
||||
workspace: workspace.workspaceId,
|
||||
id,
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(jobResult));
|
||||
}
|
||||
|
||||
async function logs(
|
||||
opts: GlobalOptions,
|
||||
id: string
|
||||
) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const jobLogs = await wmill.getJobLogs({
|
||||
workspace: workspace.workspaceId,
|
||||
id,
|
||||
});
|
||||
|
||||
console.log(jobLogs);
|
||||
}
|
||||
|
||||
async function cancel(
|
||||
opts: GlobalOptions & { reason?: string },
|
||||
id: string
|
||||
) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await wmill.cancelQueuedJob({
|
||||
workspace: workspace.workspaceId,
|
||||
id,
|
||||
requestBody: {
|
||||
reason: opts.reason ?? "Canceled via CLI",
|
||||
},
|
||||
});
|
||||
|
||||
log.info(colors.green(`Job ${id} canceled.`));
|
||||
}
|
||||
|
||||
// Shared list options to avoid repetition between default action and list subcommand
|
||||
const listOptions = (cmd: Command) =>
|
||||
cmd
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.option("--script-path <scriptPath:string>", "Filter by exact script/flow path")
|
||||
.option("--created-by <createdBy:string>", "Filter by creator username")
|
||||
.option("--running", "Show only running jobs")
|
||||
.option("--failed", "Show only failed jobs")
|
||||
.option("--success <success:boolean>", "Filter by success status (true/false)")
|
||||
.option("--limit <limit:number>", "Number of jobs to return (default 30, max 100)")
|
||||
.option("--job-kinds <jobKinds:string>", "Filter by job kinds (default: script,flow,singlestepflow)")
|
||||
.option("--label <label:string>", "Filter by job label")
|
||||
.option("--all", "Include sub-jobs (flow steps). By default only top-level jobs are shown");
|
||||
|
||||
const command = listOptions(new Command()
|
||||
.description("Manage jobs (list, inspect, cancel)"))
|
||||
.action(list as any)
|
||||
.command("list", listOptions(new Command().description("List recent jobs")))
|
||||
.action(list as any)
|
||||
.command("get", "Get job details and result")
|
||||
.arguments("<id:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(get as any)
|
||||
.command("result", "Get the result of a completed job (machine-friendly)")
|
||||
.arguments("<id:string>")
|
||||
.action(result as any)
|
||||
.command("logs", "Get job logs")
|
||||
.arguments("<id:string>")
|
||||
.action(logs as any)
|
||||
.command("cancel", "Cancel a running or queued job")
|
||||
.arguments("<id:string>")
|
||||
.option("--reason <reason:string>", "Reason for cancellation")
|
||||
.action(cancel as any);
|
||||
|
||||
export default command;
|
||||
@@ -8,6 +8,7 @@ import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace, validatePath } from "../../core/context.ts";
|
||||
import { mergeConfigWithConfigFile } from "../../core/conf.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
|
||||
import {
|
||||
@@ -163,6 +164,34 @@ export async function pushSchedule(
|
||||
}
|
||||
}
|
||||
|
||||
async function enable(opts: GlobalOptions, path: string) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await wmill.setScheduleEnabled({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
requestBody: { enabled: true },
|
||||
});
|
||||
|
||||
log.info(colors.green(`Schedule ${path} enabled.`));
|
||||
}
|
||||
|
||||
async function disable(opts: GlobalOptions, path: string) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await wmill.setScheduleEnabled({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
requestBody: { enabled: false },
|
||||
});
|
||||
|
||||
log.info(colors.yellow(`Schedule ${path} disabled.`));
|
||||
}
|
||||
|
||||
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
@@ -206,6 +235,12 @@ const command = new Command()
|
||||
"push a local schedule spec. This overrides any remote versions."
|
||||
)
|
||||
.arguments("<file_path:string> <remote_path:string>")
|
||||
.action(push as any);
|
||||
.action(push as any)
|
||||
.command("enable", "Enable a schedule")
|
||||
.arguments("<path:string>")
|
||||
.action(enable as any)
|
||||
.command("disable", "Disable a schedule")
|
||||
.arguments("<path:string>")
|
||||
.action(disable as any);
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -1501,6 +1501,42 @@ async function preview(
|
||||
}
|
||||
}
|
||||
|
||||
async function history(
|
||||
opts: GlobalOptions & { json?: boolean },
|
||||
scriptPath: string
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const versions = await wmill.getScriptHistoryByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path: scriptPath,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(versions));
|
||||
} else {
|
||||
if (versions.length === 0) {
|
||||
log.info("No version history found for " + scriptPath);
|
||||
return;
|
||||
}
|
||||
new Table()
|
||||
.header(["#", "Hash", "Deployment Message"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(
|
||||
versions.map((v, i) => [
|
||||
String(versions.length - i),
|
||||
v.script_hash,
|
||||
v.deployment_msg ?? "-",
|
||||
])
|
||||
)
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("script related commands")
|
||||
.option("--show-archived", "Enable archived scripts in output")
|
||||
@@ -1575,6 +1611,13 @@ const command = new Command()
|
||||
"-e --excludes <patterns:file[]>",
|
||||
"Comma separated patterns to specify which file to NOT take into account."
|
||||
)
|
||||
.action(generateMetadata as any);
|
||||
.action(generateMetadata as any)
|
||||
.command(
|
||||
"history",
|
||||
"show version history for a script"
|
||||
)
|
||||
.arguments("<path:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(history as any);
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { requireLogin } from "../../core/auth.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 { mergeConfigWithConfigFile } from "../../core/conf.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { formatTimestamp } from "../../utils/utils.ts";
|
||||
|
||||
async function list(opts: GlobalOptions & { json?: boolean }) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const tokens = await wmill.listTokens({
|
||||
excludeEphemeral: true,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(tokens));
|
||||
} else {
|
||||
if (tokens.length === 0) {
|
||||
log.info("No tokens found.");
|
||||
return;
|
||||
}
|
||||
new Table()
|
||||
.header(["Prefix", "Label", "Created At", "Last Used", "Expiration"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(
|
||||
tokens.map((t) => [
|
||||
t.token_prefix,
|
||||
t.label ?? "-",
|
||||
formatTimestamp(t.created_at),
|
||||
formatTimestamp(t.last_used_at),
|
||||
t.expiration ? formatTimestamp(t.expiration) : "never",
|
||||
])
|
||||
)
|
||||
.render();
|
||||
}
|
||||
}
|
||||
|
||||
async function create(
|
||||
opts: GlobalOptions & {
|
||||
label?: string;
|
||||
expiration?: string;
|
||||
}
|
||||
) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const token = await wmill.createToken({
|
||||
requestBody: {
|
||||
label: opts.label,
|
||||
expiration: opts.expiration,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(token);
|
||||
}
|
||||
|
||||
async function deleteToken(opts: GlobalOptions, tokenPrefix: string) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await wmill.deleteToken({ tokenPrefix });
|
||||
|
||||
log.info(colors.green(`Token with prefix '${tokenPrefix}' deleted.`));
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("Manage API tokens")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("list", "List API tokens")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(list as any)
|
||||
.command("create", "Create a new API token")
|
||||
.option("--label <label:string>", "Token label")
|
||||
.option("--expiration <expiration:string>", "Token expiration (ISO 8601 timestamp)")
|
||||
.action(create as any)
|
||||
.command("delete", "Delete a token by its prefix")
|
||||
.arguments("<token_prefix:string>")
|
||||
.action(deleteToken as any);
|
||||
|
||||
export default command;
|
||||
@@ -4999,6 +4999,16 @@ app related commands
|
||||
- \`--dry-run\` - Perform a dry run without making changes
|
||||
- \`--default-ts <runtime:string>\` - Default TypeScript runtime (bun or deno)
|
||||
|
||||
### audit
|
||||
|
||||
View audit logs (requires admin)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`audit list\` - List audit log entries
|
||||
- \`audit get <id:string>\` - Get a specific audit log entry
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
### config
|
||||
|
||||
Show all available wmill.yaml configuration options
|
||||
@@ -5066,6 +5076,10 @@ flow related commands
|
||||
- \`flow bootstrap <flow_path:string>\` - create a new empty flow (alias for new
|
||||
- \`--summary <summary:string>\` - flow summary
|
||||
- \`--description <description:string>\` - flow description
|
||||
- \`flow history <path:string>\` - Show version history for a flow
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`flow show-version <path:string> <version:string>\` - Show a specific version of a flow
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
### folder
|
||||
|
||||
@@ -5128,6 +5142,25 @@ Manage git-sync settings between local wmill.yaml and Windmill backend
|
||||
- \`--yes\` - Skip interactive prompts and use default behavior
|
||||
- \`--promotion <branch:string>\` - Use promotionOverrides from the specified branch instead of regular overrides
|
||||
|
||||
### group
|
||||
|
||||
Manage workspace groups
|
||||
|
||||
**Options:**
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`group list\` - List all groups in the workspace
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`group get <name:string>\` - Get group details and members
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`group create <name:string>\` - Create a new group
|
||||
- \`--summary <summary:string>\` - Group summary/description
|
||||
- \`group delete <name:string>\` - Delete a group
|
||||
- \`group add-user <name:string> <username:string>\` - Add a user to a group
|
||||
- \`group remove-user <name:string> <username:string>\` - Remove a user from a group
|
||||
|
||||
### hub
|
||||
|
||||
Hub related commands. EXPERIMENTAL. INTERNAL USE ONLY.
|
||||
@@ -5186,6 +5219,20 @@ sync local with a remote instance or the opposite (push or pull)
|
||||
- \`--show-secrets\` - Include sensitive fields (license key, JWT secret) without prompting
|
||||
- \`--instance <instance:string>\` - Name of the instance, override the active instance
|
||||
|
||||
### job
|
||||
|
||||
Manage jobs (list, inspect, cancel)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`job list\` - List recent jobs
|
||||
- \`job get <id:string>\` - Get job details and result
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`job result <id:string>\` - Get the result of a completed job (machine-friendly
|
||||
- \`job logs <id:string>\` - Get job logs
|
||||
- \`job cancel <id:string>\` - Cancel a running or queued job
|
||||
- \`--reason <reason:string>\` - Reason for cancellation
|
||||
|
||||
### jobs
|
||||
|
||||
Pull completed and queued jobs from workspace
|
||||
@@ -5272,6 +5319,8 @@ schedule related commands
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`schedule new <path:string>\` - create a new schedule locally
|
||||
- \`schedule push <file_path:string> <remote_path:string>\` - push a local schedule spec. This overrides any remote versions.
|
||||
- \`schedule enable <path:string>\` - Enable a schedule
|
||||
- \`schedule disable <path:string>\` - Disable a schedule
|
||||
|
||||
### script
|
||||
|
||||
@@ -5309,6 +5358,8 @@ script related commands
|
||||
- \`--schema-only\` - re-generate only script schema
|
||||
- \`-i --includes <patterns:file[]>\` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)
|
||||
- \`-e --excludes <patterns:file[]>\` - Comma separated patterns to specify which file to NOT take into account.
|
||||
- \`script history <path:string>\` - show version history for a script
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
### sync
|
||||
|
||||
@@ -5381,6 +5432,22 @@ sync local with a remote workspaces or the opposite (push or pull)
|
||||
- \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||
- \`--auto-metadata\` - Automatically regenerate stale metadata (locks and schemas) before pushing
|
||||
|
||||
### token
|
||||
|
||||
Manage API tokens
|
||||
|
||||
**Options:**
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`token list\` - List API tokens
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`token create\` - Create a new API token
|
||||
- \`--label <label:string>\` - Token label
|
||||
- \`--expiration <expiration:string>\` - Token expiration (ISO 8601 timestamp)
|
||||
- \`token delete <token_prefix:string>\` - Delete a token by its prefix
|
||||
|
||||
### trigger
|
||||
|
||||
trigger related commands
|
||||
|
||||
@@ -39,6 +39,10 @@ import queues from "./commands/queues/queues.ts";
|
||||
import dependencies from "./commands/dependencies/dependencies.ts";
|
||||
import init from "./commands/init/init.ts";
|
||||
import jobs from "./commands/jobs/jobs.ts";
|
||||
import job from "./commands/job/job.ts";
|
||||
import group from "./commands/group/group.ts";
|
||||
import audit from "./commands/audit/audit.ts";
|
||||
import token from "./commands/token/token.ts";
|
||||
import generateMetadata from "./commands/generate-metadata/generate-metadata.ts";
|
||||
import docs from "./commands/docs/docs.ts";
|
||||
import config from "./commands/config/config.ts";
|
||||
@@ -68,6 +72,10 @@ export {
|
||||
pull,
|
||||
push,
|
||||
workspaceAdd,
|
||||
job,
|
||||
group,
|
||||
audit,
|
||||
token,
|
||||
};
|
||||
|
||||
export const VERSION = "1.667.0";
|
||||
@@ -132,6 +140,10 @@ const command = new Command()
|
||||
.command("queues", queues)
|
||||
.command("dependencies", dependencies)
|
||||
.command("jobs", jobs)
|
||||
.command("job", job)
|
||||
.command("group", group)
|
||||
.command("audit", audit)
|
||||
.command("token", token)
|
||||
.command("generate-metadata", generateMetadata)
|
||||
.command("docs", docs)
|
||||
.command("config", config)
|
||||
|
||||
@@ -287,3 +287,7 @@ export function toCamel(s: string) {
|
||||
export function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
export function formatTimestamp(ts: string): string {
|
||||
return new Date(ts).toISOString().replace("T", " ").substring(0, 19);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user