fix: Improve CLI developer experience: error handling, sync workflow, JSON output, workspace forks (#8578)

* fix(cli): address 28 DX friction points across CLI commands

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(cli): regenerate system prompts after help text updates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): address PR review feedback

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): update removeType tests to match lenient behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): address CE/EE sync friction and improve JSON output

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): revert instance config masking to avoid breaking push flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): mask instance secrets by default with interactive prompt

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(cli): regenerate system prompts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): use stderr for errors, optimize skipped-files scan, rename --auto to --auto-metadata

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(cli): improve workspace fork lifecycle — delete-fork fallback, list-forks, --workspace override

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): update fork merge instructions to reference all merge methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): clarify skipped-files warning comment re DynFSElement traversal

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-03-28 08:41:52 +00:00
committed by GitHub
parent 95688884ce
commit 501a4ff2a9
28 changed files with 528 additions and 167 deletions

View File

@@ -13,7 +13,6 @@ export interface FlowDefinition {
properties: { [name: string]: SchemaProperty},
required: string[]
}
ws_error_handler_muted: false
}
export function defaultFlowDefinition(): FlowDefinition {
@@ -30,6 +29,5 @@ export function defaultFlowDefinition(): FlowDefinition {
properties: {},
required: []
},
ws_error_handler_muted: false,
}
}

View File

@@ -5,6 +5,7 @@ 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 { stat } from "node:fs/promises";
import * as windmillUtils from "@windmill-labs/shared-utils";
import { yamlParseFile } from "../../utils/yaml.ts";
import * as wmill from "../../../gen/services.gen.ts";
@@ -241,8 +242,26 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
await pushApp(workspace.workspaceId, remotePath, filePath);
log.info(colors.bold.underline.green("App pushed"));
// Detect raw apps by checking for raw_app.yaml or __raw_app/.raw_app suffix
const normalizedPath = filePath.endsWith(SEP) ? filePath.slice(0, -1) : filePath;
const isRawApp = normalizedPath.endsWith("__raw_app") || normalizedPath.endsWith(".raw_app");
let hasRawAppYaml = false;
if (!isRawApp) {
try {
const rawAppPath = (filePath.endsWith(SEP) ? filePath : filePath + SEP) + "raw_app.yaml";
await stat(rawAppPath);
hasRawAppYaml = true;
} catch { /* not a raw app */ }
}
if (isRawApp || hasRawAppYaml) {
const { pushRawApp } = await import("./raw_apps.ts");
await pushRawApp(workspace.workspaceId, remotePath, filePath);
log.info(colors.bold.underline.green("Raw app pushed"));
} else {
await pushApp(workspace.workspaceId, remotePath, filePath);
log.info(colors.bold.underline.green("App pushed"));
}
}
const command = new Command()

View File

@@ -236,7 +236,7 @@ async function dev(opts: GlobalOptions & SyncOptions) {
}
const command = new Command()
.description("Launch a dev server that will spawn a webserver with HMR")
.description("Launch a dev server that watches for local file changes and auto-pushes them to the remote workspace. Provides live reload for scripts and flows during development.")
.option(
"--includes <pattern...:string>",
"Filter paths givena glob pattern or path"

View File

@@ -106,7 +106,7 @@ async function docs(
const command = new Command()
.name("docs")
.description("Search Windmill documentation. Requires Enterprise Edition.")
.description("Search Windmill documentation.")
.arguments("<query:string>")
.option("--json", "Output results as JSON.")
.action(docs as any);

View File

@@ -218,6 +218,7 @@ async function push(opts: Options, filePath: string, remotePath: string) {
async function list(
opts: GlobalOptions & { showArchived?: boolean; includeDraftOnly?: boolean; json?: boolean }
) {
if (opts.json) log.setSilent(true);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
@@ -265,6 +266,16 @@ async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
console.log(colors.bold("Description:") + " " + (f.description ?? ""));
console.log(colors.bold("Edited by:") + " " + (f.edited_by ?? ""));
console.log(colors.bold("Edited at:") + " " + (f.edited_at ?? ""));
// API response type doesn't include flow value/modules — cast needed to access them
const modules = (f as any).value?.modules;
if (modules && Array.isArray(modules) && modules.length > 0) {
console.log(colors.bold("Steps:"));
for (const mod of modules) {
const type = mod.value?.type ?? "unknown";
const detail = mod.value?.language ?? mod.value?.path ?? "";
console.log(` ${mod.id}: ${type}${detail ? " (" + detail + ")" : ""}`);
}
}
}
}
@@ -275,6 +286,9 @@ async function run(
},
path: string
) {
if (opts.silent) {
log.setSilent(true);
}
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
@@ -322,7 +336,11 @@ async function run(
workspace: workspace.workspaceId,
id,
});
log.info(JSON.stringify(jobInfo.result ?? {}, null, 2));
if (opts.silent) {
console.log(JSON.stringify(jobInfo.result ?? {}));
} else {
log.info(JSON.stringify(jobInfo.result ?? {}, null, 2));
}
}
async function preview(
@@ -333,6 +351,9 @@ async function preview(
} & SyncOptions,
flowPath: string
) {
if (opts.silent) {
log.setSilent(true);
}
const useLocalPathScripts = !opts.remote;
if (useLocalPathScripts) {
opts = await mergeConfigWithConfigFile(opts);
@@ -341,14 +362,16 @@ async function preview(
await requireLogin(opts);
const codebases = useLocalPathScripts ? listSyncCodebases(opts) : [];
// Normalize path - ensure it's a directory path to a .flow folder
if (!flowPath.endsWith(".flow") && !flowPath.endsWith(".flow" + SEP)) {
// Normalize path - ensure it's a directory path to a .flow or __flow folder
const isFlowDir = flowPath.endsWith(".flow") || flowPath.endsWith(".flow" + SEP)
|| flowPath.endsWith("__flow") || flowPath.endsWith("__flow" + SEP);
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));
} else {
throw new Error(
"Flow path must be a .flow directory or a flow.yaml file"
"Flow path must be a .flow/__flow directory or a flow.yaml file"
);
}
}
@@ -428,7 +451,7 @@ async function preview(
}
if (opts.silent) {
console.log(JSON.stringify(result, null, 2));
console.log(JSON.stringify(result));
} else {
log.info(colors.bold.underline.green("Flow preview completed"));
log.info(JSON.stringify(result, null, 2));

View File

@@ -22,6 +22,7 @@ export interface FolderFile {
}
async function list(opts: GlobalOptions & { json?: boolean }) {
if (opts.json) log.setSilent(true);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);

View File

@@ -355,71 +355,102 @@ async function generateMetadata(
return colors.dim(colors.white(`[${n}/${total}]`.padEnd(maxWidth, " ")));
};
const errors: { path: string; error: string }[] = [];
// Process scripts
for (const item of scripts) {
current++;
log.info(`${formatProgress(current)} script ${item.path}`);
await generateScriptMetadataInternal(
item.path, // originalPath with extension
workspace,
opts,
false, // dryRun
true, // noStaleMessage
mismatchedWorkspaceDeps,
codebases,
false,
false, // legacyBehaviour
tree
);
try {
await generateScriptMetadataInternal(
item.path, // originalPath with extension
workspace,
opts,
false, // dryRun
true, // noStaleMessage
mismatchedWorkspaceDeps,
codebases,
false,
false, // legacyBehaviour
tree
);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
errors.push({ path: item.path, error: msg });
log.error(` Failed: ${msg}`);
}
}
// Process flows
for (const item of flows) {
current++;
const result = await generateFlowLockInternal(
item.folder.replaceAll("/", SEP),
false, // dryRun
workspace,
opts,
false,
true, // noStaleMessage
false, // legacyBehaviour
tree
);
const flowResult = result as FlowLocksResult | undefined;
const scriptsInfo = flowResult?.updatedScripts?.length
? colors.dim(colors.white(`: ${flowResult.updatedScripts.join(", ")}`))
: "";
log.info(`${formatProgress(current)} flow ${item.path}${scriptsInfo}`);
try {
const result = await generateFlowLockInternal(
item.folder.replaceAll("/", SEP),
false, // dryRun
workspace,
opts,
false,
true, // noStaleMessage
false, // legacyBehaviour
tree
);
const flowResult = result as FlowLocksResult | undefined;
const scriptsInfo = flowResult?.updatedScripts?.length
? colors.dim(colors.white(`: ${flowResult.updatedScripts.join(", ")}`))
: "";
log.info(`${formatProgress(current)} flow ${item.path}${scriptsInfo}`);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
errors.push({ path: item.path, error: msg });
log.info(`${formatProgress(current)} flow ${item.path}`);
log.error(` Failed: ${msg}`);
}
}
// Process apps
for (const item of apps) {
current++;
const result = await generateAppLocksInternal(
item.folder.replaceAll("/", SEP),
item.isRawApp!, // rawApp
false, // dryRun
workspace,
opts,
false,
true, // noStaleMessage
false, // legacyBehaviour
tree
);
const appResult = result as AppLocksResult | undefined;
const scriptsInfo = appResult?.updatedScripts?.length
? colors.dim(colors.white(`: ${appResult.updatedScripts.join(", ")}`))
: "";
log.info(`${formatProgress(current)} app ${item.path}${scriptsInfo}`);
try {
const result = await generateAppLocksInternal(
item.folder.replaceAll("/", SEP),
item.isRawApp!, // rawApp
false, // dryRun
workspace,
opts,
false,
true, // noStaleMessage
false, // legacyBehaviour
tree
);
const appResult = result as AppLocksResult | undefined;
const scriptsInfo = appResult?.updatedScripts?.length
? colors.dim(colors.white(`: ${appResult.updatedScripts.join(", ")}`))
: "";
log.info(`${formatProgress(current)} app ${item.path}${scriptsInfo}`);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
errors.push({ path: item.path, error: msg });
log.info(`${formatProgress(current)} app ${item.path}`);
log.error(` Failed: ${msg}`);
}
}
// Persist all stale workspace dep hashes (not just filtered — deps are global, not folder-scoped)
const allStaleDeps = staleItems.filter((i) => i.type === "dependencies");
await tree.persistDepsHashes(allStaleDeps.map((d) => d.path));
const succeeded = total - errors.length;
log.info("");
log.info(`Done. Updated ${colors.bold(String(total))} item(s).`);
if (errors.length > 0) {
log.info(`Done. Updated ${colors.bold(String(succeeded))}/${total} item(s). ${colors.red(String(errors.length) + " failed")}:`);
for (const { path, error } of errors) {
log.error(` ${path}: ${error}`);
}
process.exitCode = 1;
} else {
log.info(`Done. Updated ${colors.bold(String(total))} item(s).`);
}
}
const command = new Command()

View File

@@ -219,6 +219,22 @@ export async function pickInstance(
prefix: opts.prefix ?? "custom",
};
}
// Try to use the active workspace profile's remote as a fallback
if (instances.length < 1) {
try {
const ws = await getActiveWorkspace({});
if (ws?.remote && ws?.token) {
const remote = ws.remote.endsWith("/") ? ws.remote.slice(0, -1) : ws.remote;
setClient(ws.token, remote);
return {
name: ws.name,
remote: ws.remote,
token: ws.token,
prefix: ws.name,
};
}
} catch { /* ignore */ }
}
if (!allowNew && instances.length < 1) {
throw new Error("No instance found, please add one first");
}
@@ -648,9 +664,27 @@ export async function getActiveInstance(opts: {
}
}
async function getConfig(opts: InstanceSyncOptions & { outputFile?: string }) {
async function getConfig(opts: InstanceSyncOptions & { outputFile?: string; showSecrets?: boolean }) {
await pickInstance(opts, false);
const config = await wmill.getInstanceConfig();
const config = await wmill.getInstanceConfig() as any;
// In interactive mode, mask secrets by default and prompt
const hasSecrets = config?.global_settings?.license_key || config?.global_settings?.jwt_secret;
let showSecrets = opts.showSecrets ?? false;
if (!showSecrets && hasSecrets && process.stdout.isTTY && !opts.outputFile) {
log.warn("Config contains sensitive fields (license_key, jwt_secret). They are masked by default.");
log.warn("Use --show-secrets to include them, or press Y to show them now.");
showSecrets = await Confirm.prompt({ message: "Show secrets?", default: false });
} else if (!process.stdout.isTTY || opts.outputFile) {
// Non-interactive or writing to file: always include secrets
showSecrets = true;
}
if (!showSecrets && config?.global_settings) {
if (config.global_settings.license_key) config.global_settings.license_key = "***";
if (config.global_settings.jwt_secret) config.global_settings.jwt_secret = "***";
}
const yaml = yamlStringify(config as Record<string, unknown>);
if (opts.outputFile) {
await writeFile(opts.outputFile, yaml, "utf-8");
@@ -786,6 +820,7 @@ const command = new Command()
.command("get-config")
.description("Dump the current instance config (global settings + worker configs) as YAML")
.option("-o, --output-file <file:string>", "Write YAML to a file instead of stdout")
.option("--show-secrets", "Include sensitive fields (license key, JWT secret) without prompting")
.option(
"--instance <instance:string>",
"Name of the instance, override the active instance",

View File

@@ -88,6 +88,7 @@ async function push(opts: PushOptions, filePath: string, name: string) {
}
async function list(opts: GlobalOptions & { schema?: boolean; json?: boolean }) {
if (opts.json) log.setSilent(true);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
const res = await wmill.listResourceType({

View File

@@ -155,6 +155,7 @@ async function push(opts: PushOptions, filePath: string, remotePath: string) {
}
async function list(opts: GlobalOptions & { json?: boolean }) {
if (opts.json) log.setSilent(true);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
let page = 0;

View File

@@ -29,6 +29,7 @@ export interface ScheduleFile {
}
async function list(opts: GlobalOptions & { json?: boolean }) {
if (opts.json) log.setSilent(true);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
@@ -60,7 +61,7 @@ async function newSchedule(opts: GlobalOptions, path: string) {
if (e.message?.startsWith("File already exists")) throw e;
}
const template: ScheduleFile = {
schedule: "0 */6 * * *",
schedule: "0 0 */6 * * *",
on_failure: "",
script_path: "",
args: {},

View File

@@ -858,6 +858,7 @@ async function list(
json?: boolean;
}
) {
if (opts.json) log.setSilent(true);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
@@ -920,15 +921,44 @@ async function run(
},
path: string
) {
if (opts.silent) {
log.setSilent(true);
}
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
const input = opts.data ? await resolve(opts.data) : {};
const id = await wmill.runScriptByPath({
workspace: workspace.workspaceId,
path,
requestBody: input,
});
let id: string;
try {
id = await wmill.runScriptByPath({
workspace: workspace.workspaceId,
path,
requestBody: input,
});
} catch (e: any) {
if (e?.status === 404) {
// Script might exist but have a lock/deployment error — check before giving up
try {
const script = await wmill.getScriptByPath({
workspace: workspace.workspaceId,
path,
});
if (script.lock_error_logs) {
throw new Error(
`Script '${path}' has a deployment error and cannot be run:\n${script.lock_error_logs}`
);
}
} catch (lookupErr: any) {
if (lookupErr?.message?.includes("deployment error")) throw lookupErr;
// Re-throw non-404 lookup errors (e.g. auth/network issues)
if (lookupErr?.status && lookupErr.status !== 404) throw lookupErr;
}
throw new Error(
`Script '${path}' not found. Run 'wmill script list' to see available scripts.`
);
}
throw e;
}
if (!opts.silent) {
await track_job(workspace.workspaceId, id);
@@ -945,7 +975,7 @@ async function run(
).result ?? {};
if (opts.silent) {
console.log(result);
console.log(JSON.stringify(result));
} else {
log.info(JSON.stringify(result, null, 2));
}
@@ -1087,7 +1117,10 @@ async function bootstrap(
const scriptInitialCode = scriptBootstrapCode[resolvedLanguage];
if (scriptInitialCode === undefined) {
throw new Error("Language unknown");
const validLanguages = Object.keys(scriptBootstrapCode).sort().join(", ");
throw new Error(
`Unknown language '${language}'. Valid languages: ${validLanguages}`
);
}
const config = await readConfigFile();
@@ -1262,6 +1295,9 @@ async function preview(
} & SyncOptions,
filePath: string
) {
if (opts.silent) {
log.setSilent(true);
}
opts = await mergeConfigWithConfigFile(opts);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);

View File

@@ -1987,9 +1987,15 @@ export async function pull(
opts: GlobalOptions &
SyncOptions & { repository?: string; promotion?: string; branch?: string },
) {
if ((opts as any).jsonOutput) log.setSilent(true);
const originalCliOpts = { ...opts };
opts = await mergeConfigWithConfigFile(opts);
// --include-secrets overrides skipSecrets from wmill.yaml
if ((originalCliOpts as any).includeSecrets) {
opts.skipSecrets = false;
}
// Validate branch configuration early (skipped when --branch is used)
try {
await validateBranchConfiguration(opts, opts.branch);
@@ -2478,12 +2484,18 @@ function removeSuffix(str: string, suffix: string) {
export async function push(
opts: GlobalOptions & SyncOptions & { repository?: string; branch?: string },
) {
if ((opts as any).jsonOutput) log.setSilent(true);
// Save original CLI options before merging with config file
const originalCliOpts = { ...opts };
// Load configuration from wmill.yaml and merge with CLI options
opts = await mergeConfigWithConfigFile(opts);
// --include-secrets overrides skipSecrets from wmill.yaml
if ((originalCliOpts as any).includeSecrets) {
opts.skipSecrets = false;
}
// Validate branch configuration early (skipped when --branch is used)
try {
await validateBranchConfiguration(opts, opts.branch);
@@ -2617,6 +2629,7 @@ export async function push(
const tracker: ChangeTracker = await buildTracker(changes);
const autoRegenerate = !!(opts as any).autoMetadata;
const staleScripts: string[] = [];
const staleFlows: string[] = [];
const staleApps: string[] = [];
@@ -2626,7 +2639,7 @@ export async function push(
change,
workspace,
opts,
true,
!autoRegenerate, // dryRun=false when --auto is set
true,
rawWorkspaceDependencies,
codebases,
@@ -2639,11 +2652,19 @@ export async function push(
if (staleScripts.length > 0) {
log.info("");
log.warn(
"Stale scripts metadata found, you may want to update them using 'wmill script generate-metadata' before pushing:",
);
if (autoRegenerate) {
log.info("Auto-regenerated metadata for stale scripts:");
} else {
log.warn(
"Stale scripts metadata found, you may want to update them using 'wmill script generate-metadata' before pushing:",
);
}
for (const stale of staleScripts) {
log.warn(stale);
if (autoRegenerate) {
log.info(` ${stale}`);
} else {
log.warn(stale);
}
}
log.info("");
@@ -2652,7 +2673,7 @@ export async function push(
for (const change of tracker.flows) {
const stale = await generateFlowLockInternal(
change,
true,
!autoRegenerate, // dryRun=false when --auto is set
workspace,
opts,
false,
@@ -2664,11 +2685,19 @@ export async function push(
}
if (staleFlows.length > 0) {
log.warn(
"Stale flows locks found, you may want to update them using 'wmill flow generate-locks' before pushing:",
);
if (autoRegenerate) {
log.info("Auto-regenerated locks for stale flows:");
} else {
log.warn(
"Stale flows locks found, you may want to update them using 'wmill flow generate-locks' before pushing:",
);
}
for (const stale of staleFlows) {
log.warn(stale);
if (autoRegenerate) {
log.info(` ${stale}`);
} else {
log.warn(stale);
}
}
log.info("");
}
@@ -2677,7 +2706,7 @@ export async function push(
const stale = await generateAppLocksInternal(
change,
false,
true,
!autoRegenerate,
workspace,
opts,
true,
@@ -2692,7 +2721,7 @@ export async function push(
const stale = await generateAppLocksInternal(
change,
true,
true,
!autoRegenerate,
workspace,
opts,
true,
@@ -2704,15 +2733,46 @@ export async function push(
}
if (staleApps.length > 0) {
log.warn(
"Stale apps locks found, you may want to update them using 'wmill app generate-locks' before pushing:",
);
if (autoRegenerate) {
log.info("Auto-regenerated locks for stale apps:");
} else {
log.warn(
"Stale apps locks found, you may want to update them using 'wmill app generate-locks' before pushing:",
);
}
for (const stale of staleApps) {
log.warn(stale);
if (autoRegenerate) {
log.info(` ${stale}`);
} else {
log.warn(stale);
}
}
log.info("");
}
// Warn about local files for skipped types. Walks the in-memory DynFSElement tree
// (not a fresh disk scan), but does re-traverse it. Acceptable cost for a one-time check.
{
const skippedWarnings: string[] = [];
let scheduleCount = 0;
let triggerCount = 0;
for await (const entry of readDirRecursiveWithIgnore(() => false, local)) {
if (entry.isDirectory) continue;
if (!opts.includeSchedules && entry.path.endsWith(".schedule.yaml")) scheduleCount++;
if (!opts.includeTriggers && entry.path.endsWith("_trigger.yaml")) triggerCount++;
}
if (scheduleCount > 0) {
skippedWarnings.push(`Skipping ${scheduleCount} schedule file(s). Use --include-schedules or set includeSchedules: true in wmill.yaml`);
}
if (triggerCount > 0) {
skippedWarnings.push(`Skipping ${triggerCount} trigger file(s). Use --include-triggers or set includeTriggers: true in wmill.yaml`);
}
for (const warning of skippedWarnings) {
log.warn(warning);
}
if (skippedWarnings.length > 0) log.info("");
}
await fetchRemoteVersion(workspace);
log.info(
@@ -3522,6 +3582,7 @@ const command = new Command()
.option("--json", "Use JSON instead of YAML")
.option("--skip-variables", "Skip syncing variables (including secrets)")
.option("--skip-secrets", "Skip syncing only secrets variables")
.option("--include-secrets", "Include secrets in sync (overrides skipSecrets in wmill.yaml)")
.option("--skip-resources", "Skip syncing resources")
.option("--skip-resource-types", "Skip syncing resource types")
.option("--skip-scripts", "Skip syncing scripts")
@@ -3577,6 +3638,7 @@ const command = new Command()
.option("--json", "Use JSON instead of YAML")
.option("--skip-variables", "Skip syncing variables (including secrets)")
.option("--skip-secrets", "Skip syncing only secrets variables")
.option("--include-secrets", "Include secrets in sync (overrides skipSecrets in wmill.yaml)")
.option("--skip-resources", "Skip syncing resources")
.option("--skip-resource-types", "Skip syncing resource types")
.option("--skip-scripts", "Skip syncing scripts")
@@ -3626,6 +3688,7 @@ const command = new Command()
"--locks-required",
"Fail if scripts or flow inline scripts that need locks have no locks",
)
.option("--auto-metadata", "Automatically regenerate stale metadata (locks and schemas) before pushing")
.action(push as any);
export default command;

View File

@@ -308,11 +308,20 @@ const triggerTemplates: Record<TriggerType, Record<string, any>> = {
http_method: "get",
is_async: false,
requires_auth: true,
request_type: "sync",
authentication_method: "none",
is_static_website: false,
workspaced_route: false,
wrap_body: false,
raw_string: false,
},
websocket: {
script_path: "",
is_flow: false,
url: "",
filters: [],
can_return_message: false,
can_return_error_result: false,
enabled: false,
},
kafka: {
@@ -321,6 +330,7 @@ const triggerTemplates: Record<TriggerType, Record<string, any>> = {
kafka_resource_path: "",
group_id: "",
topics: [],
filters: [],
enabled: false,
},
nats: {
@@ -328,6 +338,7 @@ const triggerTemplates: Record<TriggerType, Record<string, any>> = {
is_flow: false,
nats_resource_path: "",
subjects: [],
use_jetstream: false,
enabled: false,
},
postgres: {
@@ -342,23 +353,25 @@ const triggerTemplates: Record<TriggerType, Record<string, any>> = {
script_path: "",
is_flow: false,
mqtt_resource_path: "",
topics: [],
subscribe_qos: 0,
subscribe_topics: [],
enabled: false,
},
sqs: {
script_path: "",
is_flow: false,
sqs_resource_path: "",
queue_url: "",
aws_resource_path: "",
aws_auth_resource_type: "credentials",
enabled: false,
},
gcp: {
script_path: "",
is_flow: false,
gcp_resource_path: "",
subscription_id: "",
topic_id: "",
subscription_id: "",
delivery_type: "pull",
subscription_mode: "create_update",
enabled: false,
},
email: {
@@ -437,7 +450,7 @@ async function get(opts: GlobalOptions & { json?: boolean; kind?: string }, path
} else {
console.log(colors.bold("Path:") + " " + trigger.path);
console.log(colors.bold("Kind:") + " " + kind);
console.log(colors.bold("Enabled:") + " " + (trigger.enabled ?? "-"));
console.log(colors.bold("Enabled:") + " " + (trigger.enabled ?? (trigger as any).mode ?? "-"));
console.log(colors.bold("Script Path:") + " " + (trigger.script_path ?? ""));
console.log(colors.bold("Is Flow:") + " " + (trigger.is_flow ? "true" : "false"));
}
@@ -461,6 +474,7 @@ async function listOrEmpty<T>(fn: () => Promise<T[]>): Promise<T[]> {
}
async function list(opts: GlobalOptions & { json?: boolean }) {
if (opts.json) log.setSilent(true);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);

View File

@@ -530,7 +530,7 @@ const command = new Command()
.command("remove", "Delete a user")
.arguments("<email:string>")
.action(remove as any)
.command("create-token")
.command("create-token", "Create a new API token for the authenticated user")
.option(
"--email <email:string>",
"Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.",

View File

@@ -20,6 +20,7 @@ import * as wmill from "../../../gen/services.gen.ts";
import { ListableVariable } from "../../../gen/types.gen.ts";
async function list(opts: GlobalOptions & { json?: boolean }) {
if (opts.json) log.setSilent(true);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);

View File

@@ -129,10 +129,16 @@ async function createWorkspaceFork(
const newBranchName = `${WM_FORK_PREFIX}/${clonedBranchName}/${workspaceId}`
log.info(`Created forked workspace ${trueWorkspaceId}. To start contributing to your fork, create and push edits to the branch \`${newBranchName}\` by using the command:
\t`+colors.white(`git checkout -b ${newBranchName}`) + `
When doing operations on the forked workspace, it will use the remote setup in gitBranches for the branch it was forked from.`);
When doing operations on the forked workspace, it will use the remote setup in gitBranches for the branch it was forked from.
To merge changes back to the parent workspace, you can:
- Use the Merge UI from the forked workspace home page
- Deploy individual items via the Deploy to staging/prod UI
- Use git: ` + colors.white(`git checkout ${clonedBranchName} && git merge ${newBranchName} && wmill sync push`) + `
See: https://www.windmill.dev/docs/advanced/workspace_forks`);
}
async function deleteWorkspaceFork(
@@ -141,54 +147,69 @@ async function deleteWorkspaceFork(
},
name: string,
) {
let forkWorkspaceId: string;
let token: string;
let remote: string;
let hasLocalProfile = false;
// Try local profile first (existing behavior)
const orgWorkspaces = await allWorkspaces(opts.configDir);
const idxOf = orgWorkspaces.findIndex((x) => x.name === name) ;
if (idxOf === -1) {
log.info(
colors.red.bold(`! Workspace profile ${name} does not exist locally`)
);
log.info("available workspace profiles:");
await list(opts);
return;
}
const idxOf = orgWorkspaces.findIndex((x) => x.name === name);
const workspace = orgWorkspaces[idxOf];
if (!workspace.workspaceId.startsWith(WM_FORK_PREFIX)) {
if (idxOf !== -1) {
const workspace = orgWorkspaces[idxOf];
if (!workspace.workspaceId.startsWith(WM_FORK_PREFIX)) {
throw new Error(
`You can only delete forked workspaces where the workspace id starts with \`${WM_FORK_PREFIX}.\` Failed while attempting to delete \`${workspace.workspaceId}\``,
);
}
forkWorkspaceId = workspace.workspaceId;
token = workspace.token;
remote = workspace.remote;
hasLocalProfile = true;
} else {
// Fallback: resolve parent workspace from branch config and construct fork ID
const parentWorkspace = await tryResolveBranchWorkspace(opts);
if (!parentWorkspace) {
throw new Error(
"Could not resolve parent workspace. Make sure you are in a git repo with gitBranches configured in wmill.yaml, or create a local workspace profile for the fork.",
);
}
forkWorkspaceId = name.startsWith(`${WM_FORK_PREFIX}-`) ? name : `${WM_FORK_PREFIX}-${name}`;
token = parentWorkspace.token;
remote = parentWorkspace.remote;
}
if (!opts.yes) {
const { Select } = await import("@cliffy/prompt/select");
const choice = await Select.prompt({
message: `Are you sure you want to delete the forked workspace with id: \`${workspace.workspaceId}\`? This action will delete the workspace `,
options: [
{ name: "Yes", value: "confirm" },
{ name: "No", value: "cancel" },
],
});
const { Select } = await import("@cliffy/prompt/select");
const choice = await Select.prompt({
message: `Are you sure you want to delete the forked workspace \`${forkWorkspaceId}\`?`,
options: [
{ name: "Yes", value: "confirm" },
{ name: "No", value: "cancel" },
],
});
if (choice === "cancel") {
log.info("Operation cancelled");
return;
}
if (choice === "cancel") {
log.info("Operation cancelled");
return;
}
}
const remote = workspace.remote
setClient(
workspace.token,
token,
remote.endsWith("/") ? remote.substring(0, remote.length - 1) : remote
);
const result = await wmill.deleteWorkspace({
workspace: workspace.workspaceId
workspace: forkWorkspaceId
});
log.info(
colors.green(`✅ Forked workspace '${workspace.workspaceId}' deleted successfully!\n${result}`),
colors.green(`✅ Forked workspace '${forkWorkspaceId}' deleted successfully!\n${result}`),
);
await removeWorkspace(name, false, opts);
if (hasLocalProfile) {
await removeWorkspace(name, false, opts);
}
}
export { createWorkspaceFork, deleteWorkspaceFork };

View File

@@ -253,8 +253,12 @@ export async function add(
"On that instance and with those credentials, the workspaces that you can access are:"
);
const workspaces = await wmill.listWorkspaces();
for (const workspace of workspaces) {
log.info(`- ${workspace.id} (name: ${workspace.name})`);
if (workspaces.length === 0) {
log.info(" (none)");
} else {
for (const workspace of workspaces) {
log.info(`- ${workspace.id} (name: ${workspace.name})`);
}
}
process.exit(1);
}
@@ -411,31 +415,94 @@ async function whoami(_opts: GlobalOptions) {
const whoamiInfo = await wmill.globalWhoami();
log.info(JSON.stringify(whoamiInfo, null, 2));
const activeName = await getActiveWorkspaceName(_opts);
log.info("Active: " + colors.green.bold(activeName || "none"));
const { getCurrentGitBranch, getOriginalBranchForWorkspaceForks } = await import("../../utils/git.ts");
const branch = getCurrentGitBranch();
const originalBranch = branch ? getOriginalBranchForWorkspaceForks(branch) : null;
if (originalBranch) {
const { resolveWorkspace } = await import("../../core/context.ts");
try {
const ws = await resolveWorkspace(_opts);
log.info("Active: " + colors.green.bold(`${activeName || "none"}`) + ` (fork workspace: ${ws.workspaceId})`);
} catch {
log.info("Active: " + colors.green.bold(activeName || "none") + " (fork branch)");
}
} else {
log.info("Active: " + colors.green.bold(activeName || "none"));
}
}
async function listRemote(_opts: GlobalOptions) {
const { resolveWorkspace } = await import("../../core/context.ts");
const workspace = await resolveWorkspace(_opts);
await requireLogin(_opts);
let remote: string;
if (_opts.baseUrl && _opts.token && !_opts.workspace) {
// Allow listing workspaces with just --base-url and --token (no --workspace needed)
const { setClient } = await import("../../core/client.ts");
remote = new URL(_opts.baseUrl).toString();
setClient(_opts.token, remote.replace(/\/$/, ""));
} else {
const { resolveWorkspace } = await import("../../core/context.ts");
const workspace = await resolveWorkspace(_opts);
await requireLogin(_opts);
remote = workspace.remote;
}
const userWorkspaces = await wmill.listUserWorkspaces();
const hasForks = userWorkspaces.workspaces.some((x) => x.parent_workspace_id);
const headers = hasForks
? ["id", "name", "username", "fork of", "disabled"]
: ["id", "name", "username", "disabled"];
new Table()
.header(["id", "name", "username", "disabled"])
.header(headers)
.padding(2)
.border(true)
.body(
userWorkspaces.workspaces.map((x) => [
userWorkspaces.workspaces.map((x) => {
const row = [
x.id,
x.name,
x.username,
];
if (hasForks) row.push(x.parent_workspace_id ?? "-");
row.push(x.disabled ? colors.red("true") : "false");
return row;
})
)
.render();
log.info(`Remote: ${colors.bold(remote)}`);
log.info(`Logged in as: ${colors.green.bold(userWorkspaces.email)}`);
}
async function listForks(_opts: GlobalOptions) {
const { resolveWorkspace } = await import("../../core/context.ts");
const workspace = await resolveWorkspace(_opts);
await requireLogin(_opts);
const userWorkspaces = await wmill.listUserWorkspaces();
const forks = userWorkspaces.workspaces.filter((w) => w.parent_workspace_id);
if (forks.length === 0) {
log.info("No forked workspaces found.");
return;
}
new Table()
.header(["id", "name", "fork of", "username"])
.padding(2)
.border(true)
.body(
forks.map((x) => [
x.id,
x.name,
x.parent_workspace_id ?? "",
x.username,
x.disabled ? colors.red("true") : "false",
])
)
.render();
log.info(`Remote: ${colors.bold(workspace.remote)}`);
log.info(`Logged in as: ${colors.green.bold(userWorkspaces.email)}`);
}
export async function getActiveWorkspaceOrFallback(opts: GlobalOptions) {
@@ -566,8 +633,11 @@ const command = new Command()
.command("list-remote")
.description("List workspaces on the remote server that you have access to")
.action(listRemote as any)
.command("list-forks")
.description("List forked workspaces on the remote server")
.action(listForks as any)
.command("bind")
.description("Bind the current Git branch to the active workspace")
.description("Bind the current Git branch to the active workspace. This adds the branch to gitBranches in wmill.yaml so sync operations use the correct workspace for each branch.")
.option("--branch, --env <branch:string>", "Specify branch/environment (defaults to current)")
.action((opts) => bind(opts as any, true))
.command("unbind")

View File

@@ -195,15 +195,18 @@ export function getWmillYamlPath(): string | null {
return findWmillYaml();
}
export async function readConfigFile(): Promise<SyncOptions> {
export async function readConfigFile(opts?: { warnIfMissing?: boolean }): Promise<SyncOptions> {
const warnIfMissing = opts?.warnIfMissing ?? true;
try {
// First, try to find wmill.yaml recursively
const wmillYamlPath = findWmillYaml();
if (!wmillYamlPath) {
log.warn(
"No wmill.yaml found. Use 'wmill init' to bootstrap it."
);
if (warnIfMissing) {
log.warn(
"No wmill.yaml found. Use 'wmill init' to bootstrap it."
);
}
return {};
}

View File

@@ -262,8 +262,8 @@ export async function tryResolveBranchWorkspace(
}
}
// Read wmill.yaml to check for branch workspace configuration
const config = await readConfigFile();
// Read wmill.yaml to check for branch workspace configuration (silent — just probing)
const config = await readConfigFile({ warnIfMissing: false });
const branchConfig = config.gitBranches?.[currentBranch];
// Check if branch has workspace configuration
@@ -458,15 +458,16 @@ export async function resolveWorkspace(
const branch = branchOverride ?? getCurrentGitBranch();
// Try explicit workspace flag first (should override branch-based resolution). Unless it's a
// forked workspace, that we detect through the branch name (only when not using branchOverride)
// forked workspace, that we detect through the branch name (only when not using branchOverride
// and --workspace was not explicitly provided)
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)) {
if (branchOverride || opts.workspace || !branch || !branch.startsWith(WM_FORK_PREFIX)) {
return workspace;
} else {
log.info(
`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}\``
`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}\`. Use --workspace to override.`
);
}
} else if (opts.workspace) {

View File

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

View File

@@ -5018,14 +5018,14 @@ workspace dependencies related commands
### dev
Launch a dev server that will spawn a webserver with HMR
Launch a dev server that watches for local file changes and auto-pushes them to the remote workspace. Provides live reload for scripts and flows during development.
**Options:**
- \`--includes <pattern...:string>\` - Filter paths givena glob pattern or path
### docs
Search Windmill documentation. Requires Enterprise Edition.
Search Windmill documentation.
**Arguments:** \`<query:string>\`
@@ -5183,6 +5183,7 @@ sync local with a remote instance or the opposite (push or pull)
- \`instance whoami\` - Display information about the currently logged-in user
- \`instance get-config\` - Dump the current instance config (global settings + worker configs) as YAML
- \`-o, --output-file <file:string>\` - Write YAML to a file instead of stdout
- \`--show-secrets\` - Include sensitive fields (license key, JWT secret) without prompting
- \`--instance <instance:string>\` - Name of the instance, override the active instance
### jobs
@@ -5322,6 +5323,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- \`--json\` - Use JSON instead of YAML
- \`--skip-variables\` - Skip syncing variables (including secrets)
- \`--skip-secrets\` - Skip syncing only secrets variables
- \`--include-secrets\` - Include secrets in sync (overrides skipSecrets in wmill.yaml)
- \`--skip-resources\` - Skip syncing resources
- \`--skip-resource-types\` - Skip syncing resource types
- \`--skip-scripts\` - Skip syncing scripts
@@ -5351,6 +5353,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- \`--json\` - Use JSON instead of YAML
- \`--skip-variables\` - Skip syncing variables (including secrets)
- \`--skip-secrets\` - Skip syncing only secrets variables
- \`--include-secrets\` - Include secrets in sync (overrides skipSecrets in wmill.yaml)
- \`--skip-resources\` - Skip syncing resources
- \`--skip-resource-types\` - Skip syncing resource types
- \`--skip-scripts\` - Skip syncing scripts
@@ -5376,6 +5379,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- \`--branch, --env <branch:string>\` - Override the current git branch/environment (works even outside a git repository)
- \`--lint\` - Run lint validation before pushing
- \`--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
### trigger
@@ -5406,7 +5410,7 @@ user related commands
- \`--company <company:string>\` - Specify to set the company of the new user.
- \`--name <name:string>\` - Specify to set the name of the new user.
- \`user remove <email:string>\` - Delete a user
- \`user create-token\`
- \`user create-token\` - Create a new API token for the authenticated user
- \`--email <email:string>\` - Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.
- \`--password <password:string>\` - Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.
@@ -5474,7 +5478,8 @@ workspace related commands
- \`workspace whoami\` - Show the currently active user
- \`workspace list\` - List local workspace profiles
- \`workspace list-remote\` - List workspaces on the remote server that you have access to
- \`workspace bind\` - Bind the current Git branch to the active workspace
- \`workspace list-forks\` - List forked workspaces on the remote server
- \`workspace bind\` - Bind the current Git branch to the active workspace. This adds the branch to gitBranches in wmill.yaml so sync operations use the correct workspace for each branch.
- \`--branch, --env <branch:string>\` - Specify branch/environment (defaults to current)
- \`workspace unbind\` - Remove workspace binding from the current Git branch
- \`--branch, --env <branch:string>\` - Specify branch/environment (defaults to current)

View File

@@ -218,11 +218,22 @@ async function main() {
await command.parse(args);
} catch (e) {
if (e && typeof e === "object" && "name" in e && e.name === "ApiError") {
console.log(
"Server failed. " + (e as any).statusText + ": " + (e as any).body
const body = (e as any).body;
const bodyStr = typeof body === "object" && body !== null ? JSON.stringify(body) : body;
log.error(
"Server failed. " + (e as any).statusText + ": " + bodyStr
);
} else if (e instanceof Error) {
log.error(e.message);
} else if (e !== undefined && e !== null) {
log.error(String(e));
}
throw e;
const isDebug =
process.argv.includes("--verbose") || process.argv.includes("--debug");
if (isDebug) {
throw e;
}
process.exitCode = 1;
}
}

View File

@@ -358,12 +358,16 @@ export function removeType(str: string, type: string) {
const normalizedStr = path.normalize(str).replaceAll(SEP, "/");
if (
!normalizedStr.endsWith("." + type + ".yaml") &&
!normalizedStr.endsWith("." + type + ".json")
normalizedStr.endsWith("." + type + ".yaml") ||
normalizedStr.endsWith("." + type + ".json")
) {
throw new Error(str + " does not end with ." + type + ".(yaml|json)");
return normalizedStr.slice(0, normalizedStr.length - type.length - 6);
}
return normalizedStr.slice(0, normalizedStr.length - type.length - 6);
// Accept clean paths without the type suffix (e.g. "f/folder/name" instead of "f/folder/name.schedule.yaml")
if (normalizedStr.includes("." + type)) {
log.debug(`Path '${str}' contains '.${type}' but doesn't end with '.${type}.(yaml|json)' — treating as clean path`);
}
return normalizedStr;
}
/**

View File

@@ -203,12 +203,12 @@ describe("removeType", () => {
expect(removeType("f/test/my_var.variable.json", "variable")).toBe("f/test/my_var");
});
test("throws for wrong type suffix", () => {
expect(() => removeType("f/test/my_var.variable.yaml", "resource")).toThrow();
test("passes through path with wrong type suffix as clean path", () => {
expect(removeType("f/test/my_var.variable.yaml", "resource")).toBe("f/test/my_var.variable.yaml");
});
test("throws for no type suffix", () => {
expect(() => removeType("f/test/my_script.ts", "variable")).toThrow();
test("passes through path with no type suffix as clean path", () => {
expect(removeType("f/test/my_script.ts", "variable")).toBe("f/test/my_script.ts");
});
});

View File

@@ -60,14 +60,14 @@ workspace dependencies related commands
### dev
Launch a dev server that will spawn a webserver with HMR
Launch a dev server that watches for local file changes and auto-pushes them to the remote workspace. Provides live reload for scripts and flows during development.
**Options:**
- `--includes <pattern...:string>` - Filter paths givena glob pattern or path
### docs
Search Windmill documentation. Requires Enterprise Edition.
Search Windmill documentation.
**Arguments:** `<query:string>`
@@ -225,6 +225,7 @@ sync local with a remote instance or the opposite (push or pull)
- `instance whoami` - Display information about the currently logged-in user
- `instance get-config` - Dump the current instance config (global settings + worker configs) as YAML
- `-o, --output-file <file:string>` - Write YAML to a file instead of stdout
- `--show-secrets` - Include sensitive fields (license key, JWT secret) without prompting
- `--instance <instance:string>` - Name of the instance, override the active instance
### jobs
@@ -364,6 +365,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- `--json` - Use JSON instead of YAML
- `--skip-variables` - Skip syncing variables (including secrets)
- `--skip-secrets` - Skip syncing only secrets variables
- `--include-secrets` - Include secrets in sync (overrides skipSecrets in wmill.yaml)
- `--skip-resources` - Skip syncing resources
- `--skip-resource-types` - Skip syncing resource types
- `--skip-scripts` - Skip syncing scripts
@@ -393,6 +395,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- `--json` - Use JSON instead of YAML
- `--skip-variables` - Skip syncing variables (including secrets)
- `--skip-secrets` - Skip syncing only secrets variables
- `--include-secrets` - Include secrets in sync (overrides skipSecrets in wmill.yaml)
- `--skip-resources` - Skip syncing resources
- `--skip-resource-types` - Skip syncing resource types
- `--skip-scripts` - Skip syncing scripts
@@ -418,6 +421,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- `--branch, --env <branch:string>` - Override the current git branch/environment (works even outside a git repository)
- `--lint` - Run lint validation before pushing
- `--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
### trigger
@@ -448,7 +452,7 @@ user related commands
- `--company <company:string>` - Specify to set the company of the new user.
- `--name <name:string>` - Specify to set the name of the new user.
- `user remove <email:string>` - Delete a user
- `user create-token`
- `user create-token` - Create a new API token for the authenticated user
- `--email <email:string>` - Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.
- `--password <password:string>` - Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.
@@ -516,7 +520,8 @@ workspace related commands
- `workspace whoami` - Show the currently active user
- `workspace list` - List local workspace profiles
- `workspace list-remote` - List workspaces on the remote server that you have access to
- `workspace bind` - Bind the current Git branch to the active workspace
- `workspace list-forks` - List forked workspaces on the remote server
- `workspace bind` - Bind the current Git branch to the active workspace. This adds the branch to gitBranches in wmill.yaml so sync operations use the correct workspace for each branch.
- `--branch, --env <branch:string>` - Specify branch/environment (defaults to current)
- `workspace unbind` - Remove workspace binding from the current Git branch
- `--branch, --env <branch:string>` - Specify branch/environment (defaults to current)

View File

@@ -1587,14 +1587,14 @@ workspace dependencies related commands
### dev
Launch a dev server that will spawn a webserver with HMR
Launch a dev server that watches for local file changes and auto-pushes them to the remote workspace. Provides live reload for scripts and flows during development.
**Options:**
- \`--includes <pattern...:string>\` - Filter paths givena glob pattern or path
### docs
Search Windmill documentation. Requires Enterprise Edition.
Search Windmill documentation.
**Arguments:** \`<query:string>\`
@@ -1752,6 +1752,7 @@ sync local with a remote instance or the opposite (push or pull)
- \`instance whoami\` - Display information about the currently logged-in user
- \`instance get-config\` - Dump the current instance config (global settings + worker configs) as YAML
- \`-o, --output-file <file:string>\` - Write YAML to a file instead of stdout
- \`--show-secrets\` - Include sensitive fields (license key, JWT secret) without prompting
- \`--instance <instance:string>\` - Name of the instance, override the active instance
### jobs
@@ -1891,6 +1892,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- \`--json\` - Use JSON instead of YAML
- \`--skip-variables\` - Skip syncing variables (including secrets)
- \`--skip-secrets\` - Skip syncing only secrets variables
- \`--include-secrets\` - Include secrets in sync (overrides skipSecrets in wmill.yaml)
- \`--skip-resources\` - Skip syncing resources
- \`--skip-resource-types\` - Skip syncing resource types
- \`--skip-scripts\` - Skip syncing scripts
@@ -1920,6 +1922,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- \`--json\` - Use JSON instead of YAML
- \`--skip-variables\` - Skip syncing variables (including secrets)
- \`--skip-secrets\` - Skip syncing only secrets variables
- \`--include-secrets\` - Include secrets in sync (overrides skipSecrets in wmill.yaml)
- \`--skip-resources\` - Skip syncing resources
- \`--skip-resource-types\` - Skip syncing resource types
- \`--skip-scripts\` - Skip syncing scripts
@@ -1945,6 +1948,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- \`--branch, --env <branch:string>\` - Override the current git branch/environment (works even outside a git repository)
- \`--lint\` - Run lint validation before pushing
- \`--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
### trigger
@@ -1975,7 +1979,7 @@ user related commands
- \`--company <company:string>\` - Specify to set the company of the new user.
- \`--name <name:string>\` - Specify to set the name of the new user.
- \`user remove <email:string>\` - Delete a user
- \`user create-token\`
- \`user create-token\` - Create a new API token for the authenticated user
- \`--email <email:string>\` - Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.
- \`--password <password:string>\` - Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.
@@ -2043,7 +2047,8 @@ workspace related commands
- \`workspace whoami\` - Show the currently active user
- \`workspace list\` - List local workspace profiles
- \`workspace list-remote\` - List workspaces on the remote server that you have access to
- \`workspace bind\` - Bind the current Git branch to the active workspace
- \`workspace list-forks\` - List forked workspaces on the remote server
- \`workspace bind\` - Bind the current Git branch to the active workspace. This adds the branch to gitBranches in wmill.yaml so sync operations use the correct workspace for each branch.
- \`--branch, --env <branch:string>\` - Specify branch/environment (defaults to current)
- \`workspace unbind\` - Remove workspace binding from the current Git branch
- \`--branch, --env <branch:string>\` - Specify branch/environment (defaults to current)

View File

@@ -65,14 +65,14 @@ workspace dependencies related commands
### dev
Launch a dev server that will spawn a webserver with HMR
Launch a dev server that watches for local file changes and auto-pushes them to the remote workspace. Provides live reload for scripts and flows during development.
**Options:**
- `--includes <pattern...:string>` - Filter paths givena glob pattern or path
### docs
Search Windmill documentation. Requires Enterprise Edition.
Search Windmill documentation.
**Arguments:** `<query:string>`
@@ -230,6 +230,7 @@ sync local with a remote instance or the opposite (push or pull)
- `instance whoami` - Display information about the currently logged-in user
- `instance get-config` - Dump the current instance config (global settings + worker configs) as YAML
- `-o, --output-file <file:string>` - Write YAML to a file instead of stdout
- `--show-secrets` - Include sensitive fields (license key, JWT secret) without prompting
- `--instance <instance:string>` - Name of the instance, override the active instance
### jobs
@@ -369,6 +370,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- `--json` - Use JSON instead of YAML
- `--skip-variables` - Skip syncing variables (including secrets)
- `--skip-secrets` - Skip syncing only secrets variables
- `--include-secrets` - Include secrets in sync (overrides skipSecrets in wmill.yaml)
- `--skip-resources` - Skip syncing resources
- `--skip-resource-types` - Skip syncing resource types
- `--skip-scripts` - Skip syncing scripts
@@ -398,6 +400,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- `--json` - Use JSON instead of YAML
- `--skip-variables` - Skip syncing variables (including secrets)
- `--skip-secrets` - Skip syncing only secrets variables
- `--include-secrets` - Include secrets in sync (overrides skipSecrets in wmill.yaml)
- `--skip-resources` - Skip syncing resources
- `--skip-resource-types` - Skip syncing resource types
- `--skip-scripts` - Skip syncing scripts
@@ -423,6 +426,7 @@ sync local with a remote workspaces or the opposite (push or pull)
- `--branch, --env <branch:string>` - Override the current git branch/environment (works even outside a git repository)
- `--lint` - Run lint validation before pushing
- `--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
### trigger
@@ -453,7 +457,7 @@ user related commands
- `--company <company:string>` - Specify to set the company of the new user.
- `--name <name:string>` - Specify to set the name of the new user.
- `user remove <email:string>` - Delete a user
- `user create-token`
- `user create-token` - Create a new API token for the authenticated user
- `--email <email:string>` - Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.
- `--password <password:string>` - Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.
@@ -521,7 +525,8 @@ workspace related commands
- `workspace whoami` - Show the currently active user
- `workspace list` - List local workspace profiles
- `workspace list-remote` - List workspaces on the remote server that you have access to
- `workspace bind` - Bind the current Git branch to the active workspace
- `workspace list-forks` - List forked workspaces on the remote server
- `workspace bind` - Bind the current Git branch to the active workspace. This adds the branch to gitBranches in wmill.yaml so sync operations use the correct workspace for each branch.
- `--branch, --env <branch:string>` - Specify branch/environment (defaults to current)
- `workspace unbind` - Remove workspace binding from the current Git branch
- `--branch, --env <branch:string>` - Specify branch/environment (defaults to current)