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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user