feat(cli): wmill-lock.yaml v2 for easier git merge diffs

This commit is contained in:
Ruben Fiszel
2025-07-31 14:23:38 +00:00
parent 205674a72a
commit ef3e2353a7
5 changed files with 529 additions and 447 deletions
Generated
+10
View File
@@ -59,6 +59,7 @@
"jsr:@windmill-labs/cliffy-table@1.0.0-rc.5": "1.0.0-rc.5",
"jsr:@windmill-labs/cliffy-table@^1.0.0-rc.5": "1.0.0-rc.5",
"npm:@ayonli/jsext@*": "1.8.0",
"npm:@types/node@*": "22.12.0",
"npm:centdix-utils@*": "1.0.15",
"npm:diff@*": "8.0.2",
"npm:es-main@*": "1.3.0",
@@ -378,6 +379,12 @@
"@isaacs/balanced-match"
]
},
"@types/node@22.12.0": {
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
"dependencies": [
"undici-types"
]
},
"accepts@2.0.0": {
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"dependencies": [
@@ -905,6 +912,9 @@
"mime-types"
]
},
"undici-types@6.20.0": {
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
"unpipe@1.0.0": {
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
},
+368 -373
View File
@@ -1,11 +1,11 @@
import {
Command,
CompletionsCommand,
UpgradeCommand,
colors,
esMain,
log,
yamlStringify,
Command,
CompletionsCommand,
UpgradeCommand,
colors,
esMain,
log,
yamlStringify,
} from "./deps.ts";
import flow from "./flow.ts";
import app from "./apps.ts";
@@ -40,26 +40,26 @@ import { readLockfile } from "./metadata.ts";
import { FLOW_GUIDANCE } from "./flow_guidance.ts";
export {
flow,
app,
script,
workspace,
resource,
resourceType,
user,
variable,
hub,
folder,
schedule,
trigger,
sync,
gitsyncSettings,
instance,
dev,
hubPull,
pull,
push,
workspaceAdd,
flow,
app,
script,
workspace,
resource,
resourceType,
user,
variable,
hub,
folder,
schedule,
trigger,
sync,
gitsyncSettings,
instance,
dev,
hubPull,
pull,
push,
workspaceAdd,
};
// addEventListener("error", (event) => {
@@ -72,230 +72,226 @@ export {
export const VERSION = "1.516.0";
const command = new Command()
.name("wmill")
.action(() =>
log.info(`Welcome to Windmill CLI ${VERSION}. Use -h for help.`),
)
.description("Windmill CLI")
.name("wmill")
.action(() =>
log.info(`Welcome to Windmill CLI ${VERSION}. Use -h for help.`)
)
.description("Windmill CLI")
.globalOption(
"--workspace <workspace:string>",
"Specify the target workspace. This overrides the default workspace.",
)
.globalOption("--debug --verbose", "Show debug/verbose logs")
.globalOption(
"--show-diffs",
"Show diff informations when syncing (may show sensitive informations)",
)
.globalOption(
"--token <token:string>",
"Specify an API token. This will override any stored token.",
)
.globalOption(
"--base-url <baseUrl:string>",
"Specify the base URL of the API. If used, --token and --workspace are required and no local remote/workspace already set will be used.",
)
.globalOption(
"--config-dir <configDir:string>",
"Specify a custom config directory. Overrides WMILL_CONFIG_DIR environment variable and default ~/.config location.",
)
.env(
"HEADERS <headers:string>",
"Specify headers to use for all requests. e.g: \"HEADERS='h1: v1, h2: v2'\"",
)
.version(VERSION)
.versionOption(false)
.command("init", "Bootstrap a windmill project with a wmill.yaml file")
.option("--use-default", "Use default settings without checking backend")
.option("--use-backend", "Use backend git-sync settings if available")
.option(
"--repository <repo:string>",
"Specify repository path (e.g., u/user/repo) when using backend settings",
)
.action(
async (
opts: {
useDefault?: boolean;
useBackend?: boolean;
repository?: string;
workspace?: string;
debug?: unknown;
showDiffs?: boolean;
token?: string;
baseUrl?: string;
configDir?: string;
},
) => {
if (await Deno.stat("wmill.yaml").catch(() => null)) {
log.error(colors.red("wmill.yaml already exists"));
} else {
// Import DEFAULT_SYNC_OPTIONS from conf.ts
const { DEFAULT_SYNC_OPTIONS } = await import("./conf.ts");
.globalOption(
"--workspace <workspace:string>",
"Specify the target workspace. This overrides the default workspace."
)
.globalOption("--debug --verbose", "Show debug/verbose logs")
.globalOption(
"--show-diffs",
"Show diff informations when syncing (may show sensitive informations)"
)
.globalOption(
"--token <token:string>",
"Specify an API token. This will override any stored token."
)
.globalOption(
"--base-url <baseUrl:string>",
"Specify the base URL of the API. If used, --token and --workspace are required and no local remote/workspace already set will be used."
)
.globalOption(
"--config-dir <configDir:string>",
"Specify a custom config directory. Overrides WMILL_CONFIG_DIR environment variable and default ~/.config location."
)
.env(
"HEADERS <headers:string>",
"Specify headers to use for all requests. e.g: \"HEADERS='h1: v1, h2: v2'\""
)
.version(VERSION)
.versionOption(false)
.command("init", "Bootstrap a windmill project with a wmill.yaml file")
.option("--use-default", "Use default settings without checking backend")
.option("--use-backend", "Use backend git-sync settings if available")
.option(
"--repository <repo:string>",
"Specify repository path (e.g., u/user/repo) when using backend settings"
)
.action(
async (opts: {
useDefault?: boolean;
useBackend?: boolean;
repository?: string;
workspace?: string;
debug?: unknown;
showDiffs?: boolean;
token?: string;
baseUrl?: string;
configDir?: string;
}) => {
if (await Deno.stat("wmill.yaml").catch(() => null)) {
log.error(colors.red("wmill.yaml already exists"));
} else {
// Import DEFAULT_SYNC_OPTIONS from conf.ts
const { DEFAULT_SYNC_OPTIONS } = await import("./conf.ts");
// Create initial config with defaults
const initialConfig = {
defaultTs: DEFAULT_SYNC_OPTIONS.defaultTs,
includes: DEFAULT_SYNC_OPTIONS.includes,
excludes: DEFAULT_SYNC_OPTIONS.excludes,
codebases: DEFAULT_SYNC_OPTIONS.codebases,
skipVariables: DEFAULT_SYNC_OPTIONS.skipVariables,
skipResources: DEFAULT_SYNC_OPTIONS.skipResources,
skipSecrets: DEFAULT_SYNC_OPTIONS.skipSecrets,
skipScripts: DEFAULT_SYNC_OPTIONS.skipScripts,
skipFlows: DEFAULT_SYNC_OPTIONS.skipFlows,
skipApps: DEFAULT_SYNC_OPTIONS.skipApps,
skipFolders: DEFAULT_SYNC_OPTIONS.skipFolders,
includeSchedules: DEFAULT_SYNC_OPTIONS.includeSchedules,
includeTriggers: DEFAULT_SYNC_OPTIONS.includeTriggers,
overrides: {},
};
// Create initial config with defaults
const initialConfig = {
defaultTs: DEFAULT_SYNC_OPTIONS.defaultTs,
includes: DEFAULT_SYNC_OPTIONS.includes,
excludes: DEFAULT_SYNC_OPTIONS.excludes,
codebases: DEFAULT_SYNC_OPTIONS.codebases,
skipVariables: DEFAULT_SYNC_OPTIONS.skipVariables,
skipResources: DEFAULT_SYNC_OPTIONS.skipResources,
skipSecrets: DEFAULT_SYNC_OPTIONS.skipSecrets,
skipScripts: DEFAULT_SYNC_OPTIONS.skipScripts,
skipFlows: DEFAULT_SYNC_OPTIONS.skipFlows,
skipApps: DEFAULT_SYNC_OPTIONS.skipApps,
skipFolders: DEFAULT_SYNC_OPTIONS.skipFolders,
includeSchedules: DEFAULT_SYNC_OPTIONS.includeSchedules,
includeTriggers: DEFAULT_SYNC_OPTIONS.includeTriggers,
overrides: {},
};
await Deno.writeTextFile(
"wmill.yaml",
yamlStringify(initialConfig),
);
log.info(
colors.green("wmill.yaml created with default settings"),
);
await Deno.writeTextFile("wmill.yaml", yamlStringify(initialConfig));
log.info(colors.green("wmill.yaml created with default settings"));
// Create lock file
await readLockfile();
// Create lock file
await readLockfile();
// Check for backend git-sync settings unless --use-default is specified
if (!opts.useDefault) {
try {
const { requireLogin } = await import("./auth.ts");
const { resolveWorkspace } = await import("./context.ts");
// Check for backend git-sync settings unless --use-default is specified
if (!opts.useDefault) {
try {
const { requireLogin } = await import("./auth.ts");
const { resolveWorkspace } = await import("./context.ts");
// Check if user has workspace configured
const { getActiveWorkspace } = await import(
"./workspace.ts"
);
const activeWorkspace = await getActiveWorkspace(opts as GlobalOptions);
// Check if user has workspace configured
const { getActiveWorkspace } = await import("./workspace.ts");
const activeWorkspace = await getActiveWorkspace(
opts as GlobalOptions
);
if (!activeWorkspace) {
log.info(
"No workspace configured. Using default settings.",
);
log.info(
"You can configure a workspace later with 'wmill workspace add'",
);
return;
}
await requireLogin(opts as GlobalOptions);
const workspace = await resolveWorkspace(opts as GlobalOptions);
const wmill = await import("./gen/services.gen.ts");
const settings = await wmill.getSettings({
workspace: workspace.workspaceId,
});
if (
settings.git_sync?.repositories &&
settings.git_sync.repositories.length > 0
) {
let useBackendSettings = opts.useBackend;
// If repository is specified, implicitly use backend settings
if (opts.repository && !opts.useDefault) {
useBackendSettings = true;
}
if (useBackendSettings === undefined) {
// Interactive prompt
const { Select } = await import("./deps.ts");
const choice = await Select.prompt({
message:
"Git-sync settings found on backend. What would you like to do?",
options: [
{
name: "Use backend git-sync settings",
value: "backend",
},
{
name: "Use default settings",
value: "default",
},
{
name: "Cancel",
value: "cancel",
},
],
});
if (choice === "cancel") {
// Clean up the created files
try {
await Deno.remove("wmill.yaml");
await Deno.remove("wmill-lock.yaml");
} catch (e) {
// Ignore cleanup errors
}
log.info("Init cancelled");
Deno.exit(0);
}
useBackendSettings = choice === "backend";
}
if (useBackendSettings) {
log.info(
"Applying git-sync settings from backend...",
);
// Import and run the pull git-sync settings logic
const { pullGitSyncSettings } = await import(
"./gitsync-settings.ts"
);
await pullGitSyncSettings({
...(opts as GlobalOptions),
repository: opts.repository,
jsonOutput: false,
diff: false,
replace: true, // Auto-replace when using backend settings during init
});
log.info(
colors.green(
"Git-sync settings applied from backend",
),
);
}
}
} catch (error) {
// If there's an error checking backend settings, just continue with defaults
const errorMessage = error instanceof Error ? error.message : String(error);
log.warn(
`Could not check backend for git-sync settings: ${errorMessage}`,
);
log.info("Continuing with default settings");
}
}
if (!activeWorkspace) {
log.info("No workspace configured. Using default settings.");
log.info(
"You can configure a workspace later with 'wmill workspace add'"
);
return;
}
// Create .cursor/rules directory and files with SCRIPT_GUIDANCE content
try {
const scriptGuidanceContent = SCRIPT_GUIDANCE;
const flowGuidanceContent = FLOW_GUIDANCE;
// Create .cursor/rules directory
await Deno.mkdir(".cursor/rules", { recursive: true });
// Create windmill.mdc file
if (!await Deno.stat(".cursor/rules/script.mdc").catch(() => null)) {
await Deno.writeTextFile(".cursor/rules/script.mdc", scriptGuidanceContent);
log.info(colors.green("Created .cursor/rules/script.mdc"));
await requireLogin(opts as GlobalOptions);
const workspace = await resolveWorkspace(opts as GlobalOptions);
const wmill = await import("./gen/services.gen.ts");
const settings = await wmill.getSettings({
workspace: workspace.workspaceId,
});
if (
settings.git_sync?.repositories &&
settings.git_sync.repositories.length > 0
) {
let useBackendSettings = opts.useBackend;
// If repository is specified, implicitly use backend settings
if (opts.repository && !opts.useDefault) {
useBackendSettings = true;
}
if (useBackendSettings === undefined) {
// Interactive prompt
const { Select } = await import("./deps.ts");
const choice = await Select.prompt({
message:
"Git-sync settings found on backend. What would you like to do?",
options: [
{
name: "Use backend git-sync settings",
value: "backend",
},
{
name: "Use default settings",
value: "default",
},
{
name: "Cancel",
value: "cancel",
},
],
});
if (choice === "cancel") {
// Clean up the created files
try {
await Deno.remove("wmill.yaml");
await Deno.remove("wmill-lock.yaml");
} catch (e) {
// Ignore cleanup errors
}
log.info("Init cancelled");
Deno.exit(0);
}
if (!await Deno.stat(".cursor/rules/flow.mdc").catch(() => null)) {
await Deno.writeTextFile(".cursor/rules/flow.mdc", flowGuidanceContent);
log.info(colors.green("Created .cursor/rules/flow.mdc"));
}
// Create CLAUDE.md file
if (!await Deno.stat("CLAUDE.md").catch(() => null)) {
await Deno.writeTextFile("CLAUDE.md", `
useBackendSettings = choice === "backend";
}
if (useBackendSettings) {
log.info("Applying git-sync settings from backend...");
// Import and run the pull git-sync settings logic
const { pullGitSyncSettings } = await import(
"./gitsync-settings.ts"
);
await pullGitSyncSettings({
...(opts as GlobalOptions),
repository: opts.repository,
jsonOutput: false,
diff: false,
replace: true, // Auto-replace when using backend settings during init
});
log.info(
colors.green("Git-sync settings applied from backend")
);
}
}
} catch (error) {
// If there's an error checking backend settings, just continue with defaults
const errorMessage =
error instanceof Error ? error.message : String(error);
log.warn(
`Could not check backend for git-sync settings: ${errorMessage}`
);
log.info("Continuing with default settings");
}
}
}
// Create .cursor/rules directory and files with SCRIPT_GUIDANCE content
try {
const scriptGuidanceContent = SCRIPT_GUIDANCE;
const flowGuidanceContent = FLOW_GUIDANCE;
// Create .cursor/rules directory
await Deno.mkdir(".cursor/rules", { recursive: true });
// Create windmill.mdc file
if (!(await Deno.stat(".cursor/rules/script.mdc").catch(() => null))) {
await Deno.writeTextFile(
".cursor/rules/script.mdc",
scriptGuidanceContent
);
log.info(colors.green("Created .cursor/rules/script.mdc"));
}
if (!(await Deno.stat(".cursor/rules/flow.mdc").catch(() => null))) {
await Deno.writeTextFile(
".cursor/rules/flow.mdc",
flowGuidanceContent
);
log.info(colors.green("Created .cursor/rules/flow.mdc"));
}
// Create CLAUDE.md file
if (!(await Deno.stat("CLAUDE.md").catch(() => null))) {
await Deno.writeTextFile(
"CLAUDE.md",
`
# Claude
You are a helpful assistant that can help with Windmill scripts and flows creation.
@@ -305,162 +301,161 @@ const command = new Command()
## Flow Guidance
${flowGuidanceContent}
`);
log.info(colors.green("Created CLAUDE.md"));
}
} catch (error) {
if (error instanceof Error) {
log.warn(`Could not create guidance files: ${error.message}`);
} else {
log.warn(`Could not create guidance files: ${error}`);
}
}
},
)
.command("app", app)
.command("flow", flow)
.command("script", script)
.command("workspace", workspace)
.command("resource", resource)
.command("resource-type", resourceType)
.command("user", user)
.command("variable", variable)
.command("hub", hub)
.command("folder", folder)
.command("schedule", schedule)
.command("trigger", trigger)
.command("dev", dev)
.command("sync", sync)
.command("gitsync-settings", gitsyncSettings)
.command("instance", instance)
.command("worker-groups", workerGroups)
.command("workers", workers)
.command("queues", queues)
.command("version --version", "Show version information")
.action(async (opts) => {
console.log("CLI version: " + VERSION);
try {
const provider = new NpmProvider({ package: "windmill-cli" });
const versions = await provider.getVersions("windmill-cli");
if (versions.latest !== VERSION) {
console.log(
`CLI is outdated. Latest version ${versions.latest} is available. Run \`wmill upgrade\` to update.`,
);
} else {
console.log("CLI is up to date");
}
} catch (e) {
console.warn(
`Cannot fetch latest CLI version on npmjs to check if up-to-date: ${e}`,
);
`
);
log.info(colors.green("Created CLAUDE.md"));
}
const workspace = await getActiveWorkspace(opts as GlobalOptions);
if (workspace) {
try {
const backendVersion = await fetchVersion(workspace.remote);
console.log("Backend Version: " + backendVersion);
} catch (e) {
console.warn("Cannot fetch backend version: " + e);
}
} catch (error) {
if (error instanceof Error) {
log.warn(`Could not create guidance files: ${error.message}`);
} else {
console.warn(
"Cannot fetch backend version: no active workspace selected, choose one to pick a remote to fetch version of",
);
log.warn(`Could not create guidance files: ${error}`);
}
}
}
)
.command("app", app)
.command("flow", flow)
.command("script", script)
.command("workspace", workspace)
.command("resource", resource)
.command("resource-type", resourceType)
.command("user", user)
.command("variable", variable)
.command("hub", hub)
.command("folder", folder)
.command("schedule", schedule)
.command("trigger", trigger)
.command("dev", dev)
.command("sync", sync)
.command("gitsync-settings", gitsyncSettings)
.command("instance", instance)
.command("worker-groups", workerGroups)
.command("workers", workers)
.command("queues", queues)
.command("version --version", "Show version information")
.action(async (opts) => {
console.log("CLI version: " + VERSION);
try {
const provider = new NpmProvider({ package: "windmill-cli" });
const versions = await provider.getVersions("windmill-cli");
if (versions.latest !== VERSION) {
console.log(
`CLI is outdated. Latest version ${versions.latest} is available. Run \`wmill upgrade\` to update.`
);
} else {
console.log("CLI is up to date");
}
} catch (e) {
console.warn(
`Cannot fetch latest CLI version on npmjs to check if up-to-date: ${e}`
);
}
const workspace = await getActiveWorkspace(opts as GlobalOptions);
if (workspace) {
try {
const backendVersion = await fetchVersion(workspace.remote);
console.log("Backend Version: " + backendVersion);
} catch (e) {
console.warn("Cannot fetch backend version: " + e);
}
} else {
console.warn(
"Cannot fetch backend version: no active workspace selected, choose one to pick a remote to fetch version of"
);
}
})
.command(
"upgrade",
new UpgradeCommand({
provider: new NpmProvider({ package: "windmill-cli" }),
}).error((e: any) => {
log.error(e);
log.info(
"Try running with sudo and otherwise check the result of the command: npm uninstall windmill-cli && npm install -g windmill-cli"
);
})
.command(
"upgrade",
new UpgradeCommand({
provider: new NpmProvider({ package: "windmill-cli" }),
}).error((e: any) => {
log.error(e);
log.info(
"Try running with sudo and otherwise check the result of the command: npm uninstall windmill-cli && npm install -g windmill-cli",
);
}),
)
.command("completions", new CompletionsCommand());
)
.command("completions", new CompletionsCommand());
export let showDiffs = false;
let isWin: boolean | undefined = undefined;
export async function getIsWin() {
if (isWin === undefined) {
const os = await import("node:os");
isWin = os.platform() === "win32";
}
return isWin;
if (isWin === undefined) {
const os = await import("node:os");
isWin = os.platform() === "win32";
}
return isWin;
}
async function main() {
try {
if (Deno.args.length === 0) {
command.showHelp();
}
const LOG_LEVEL =
Deno.args.includes("--verbose") || Deno.args.includes("--debug")
? "DEBUG"
: "INFO";
// const NO_COLORS = Deno.args.includes("--no-colors");
showDiffs = Deno.args.includes("--show-diffs");
log.setup({
handlers: {
console: new log.ConsoleHandler(LOG_LEVEL, {
formatter: ({ msg }) => `${msg}`,
useColors: isWin ? false : true,
}),
},
loggers: {
default: {
level: LOG_LEVEL,
handlers: ["console"],
},
},
});
log.debug("Debug logging enabled. CLI build against " + VERSION);
const extraHeaders = getHeaders();
if (extraHeaders) {
OpenAPI.HEADERS = extraHeaders;
}
await command.parse(Deno.args);
} catch (e) {
if (e.name === "ApiError") {
console.log("Server failed. " + e.statusText + ": " + e.body);
}
throw e;
try {
if (Deno.args.length === 0) {
command.showHelp();
}
const LOG_LEVEL =
Deno.args.includes("--verbose") || Deno.args.includes("--debug")
? "DEBUG"
: "INFO";
// const NO_COLORS = Deno.args.includes("--no-colors");
showDiffs = Deno.args.includes("--show-diffs");
log.setup({
handlers: {
console: new log.ConsoleHandler(LOG_LEVEL, {
formatter: ({ msg }) => `${msg}`,
useColors: isWin ? false : true,
}),
},
loggers: {
default: {
level: LOG_LEVEL,
handlers: ["console"],
},
},
});
log.debug("Debug logging enabled. CLI build against " + VERSION);
const extraHeaders = getHeaders();
if (extraHeaders) {
OpenAPI.HEADERS = extraHeaders;
}
await command.parse(Deno.args);
} catch (e) {
if (e.name === "ApiError") {
console.log("Server failed. " + e.statusText + ": " + e.body);
}
throw e;
}
}
function isMain() {
// dnt-shim-ignore
const { Deno } = globalThis as any;
// dnt-shim-ignore
const { Deno } = globalThis as any;
const isDeno = Deno != undefined;
const isDeno = Deno != undefined;
if (isDeno) {
const isMain = import.meta.main;
if (isMain) {
if (!Deno.args.includes("completions")) {
if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") {
log.warn(
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true",
);
}
}
if (isDeno) {
const isMain = import.meta.main;
if (isMain) {
if (!Deno.args.includes("completions")) {
if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") {
log.warn(
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true"
);
}
return isMain;
} else {
//@ts-ignore
return esMain.default(import.meta);
}
}
return isMain;
} else {
//@ts-ignore
return esMain.default(import.meta);
}
}
if (isMain()) {
main();
main();
}
export default command;
+43 -23
View File
@@ -20,11 +20,7 @@ import {
} from "./script_common.ts";
import { inferContentTypeFromFilePath } from "./script_common.ts";
import { GlobalDeps, exts, findGlobalDeps } from "./script.ts";
import {
FSFSElement,
findCodebase,
yamlOptions,
} from "./sync.ts";
import { FSFSElement, findCodebase, yamlOptions } from "./sync.ts";
import { generateHash, readInlinePathSync } from "./utils.ts";
import { SyncCodebase } from "./codebase.ts";
import { FlowFile } from "./flow.ts";
@@ -180,7 +176,7 @@ export async function generateFlowLockInternal(
SEP,
changedScripts,
(path: string, newPath: string) => Deno.renameSync(path, newPath),
(path: string) => Deno.removeSync(path),
(path: string) => Deno.removeSync(path)
);
//removeChangedLocks
@@ -191,7 +187,11 @@ export async function generateFlowLockInternal(
rawReqs
);
const inlineScripts = extractInlineScriptsForFlows(flowValue.value.modules, {}, SEP);
const inlineScripts = extractInlineScriptsForFlows(
flowValue.value.modules,
{},
SEP
);
inlineScripts
.filter((s) => s.path.endsWith(".lock"))
.forEach((s) => {
@@ -531,9 +531,7 @@ export async function inferSchema(
}> {
let inferedSchema: any;
if (language === "python3") {
const { parse_python } = await import(
"./wasm/py/windmill_parser_wasm.js"
);
const { parse_python } = await import("./wasm/py/windmill_parser_wasm.js");
inferedSchema = JSON.parse(parse_python(content));
} else if (language === "nativets") {
const { parse_deno } = await import("./wasm/ts/windmill_parser_wasm.js");
@@ -601,7 +599,9 @@ export async function inferSchema(
...inferedSchema.args,
];
} else if (language === "duckdb") {
const { parse_duckdb } = await import("./wasm/regex/windmill_parser_wasm.js");
const { parse_duckdb } = await import(
"./wasm/regex/windmill_parser_wasm.js"
);
inferedSchema = JSON.parse(parse_duckdb(content));
} else if (language === "graphql") {
const { parse_graphql } = await import(
@@ -816,6 +816,7 @@ export async function parseMetadataFile(
}
interface Lock {
version?: "v2";
locks?: { [path: string]: string | { [subpath: string]: string } };
}
@@ -829,7 +830,7 @@ export async function readLockfile(): Promise<Lock> {
throw new Error("Invalid lockfile");
}
} catch {
const lock = { locks: {} };
const lock = { locks: {}, version: "v2" as const };
await Deno.writeTextFile(WMILL_LOCKFILE, yamlStringify(lock, yamlOptions));
log.info(colors.green("wmill-lock.yaml created"));
@@ -837,6 +838,13 @@ export async function readLockfile(): Promise<Lock> {
}
}
function v2LockPath(path: string, subpath?: string) {
if (subpath) {
return `${path}+${subpath}`;
} else {
return path;
}
}
export async function checkifMetadataUptodate(
path: string,
hash: string,
@@ -849,9 +857,16 @@ export async function checkifMetadataUptodate(
if (!conf.locks) {
return false;
}
const obj = conf.locks?.[path];
const current = subpath && typeof obj == "object" ? obj?.[subpath] : obj;
return current == hash;
const isV2 = conf?.version == "v2";
if (isV2) {
const current = conf.locks?.[v2LockPath(path, subpath)];
return current == hash;
} else {
const obj = conf.locks?.[path];
const current = subpath && typeof obj == "object" ? obj?.[subpath] : obj;
return current == hash;
}
}
export async function generateScriptHash(
@@ -873,16 +888,21 @@ export async function updateMetadataGlobalLock(
if (!conf?.locks) {
conf.locks = {};
}
const isV2 = conf?.version == "v2";
if (subpath) {
let prev: any = conf.locks[path];
if (!prev || typeof prev != "object") {
prev = {};
conf.locks[path] = prev;
}
prev[subpath] = hash;
if (isV2) {
conf.locks[v2LockPath(path, hash)] = hash;
} else {
conf.locks[path] = hash;
if (subpath) {
let prev: any = conf.locks[path];
if (!prev || typeof prev != "object") {
prev = {};
conf.locks[path] = prev;
}
prev[subpath] = hash;
} else {
conf.locks[path] = hash;
}
}
await Deno.writeTextFile(
WMILL_LOCKFILE,
+83 -36
View File
@@ -52,10 +52,9 @@ import { assignPath } from "./windmill-utils-internal/src/path-utils/path-assign
import { extractInlineScripts as extractInlineScriptsForFlows } from "./windmill-utils-internal/src/inline-scripts/extractor.ts";
// Merge CLI options with effective settings, preserving CLI flags as overrides
function mergeCliWithEffectiveOptions<T extends GlobalOptions & SyncOptions & { repository?: string }>(
cliOpts: T,
effectiveOpts: SyncOptions
): T {
function mergeCliWithEffectiveOptions<
T extends GlobalOptions & SyncOptions & { repository?: string }
>(cliOpts: T, effectiveOpts: SyncOptions): T {
// overlay CLI options on top (undefined cliOpts won't override effectiveOpts)
return Object.assign({}, effectiveOpts, cliOpts) as T;
}
@@ -84,7 +83,7 @@ async function resolveEffectiveSyncOptions(
// Find all repository-specific overrides for this workspace
for (const key of Object.keys(localConfig.overrides)) {
if (key.startsWith(prefix) && !key.endsWith(':*')) {
if (key.startsWith(prefix) && !key.endsWith(":*")) {
const repo = key.substring(prefix.length);
if (repo) {
applicableRepos.push(repo);
@@ -107,13 +106,16 @@ async function resolveEffectiveSyncOptions(
if (isInteractive) {
const choices = [
{ name: "Use top-level settings (no repository-specific override)", value: "" },
...applicableRepos.map(repo => ({ name: repo, value: repo }))
{
name: "Use top-level settings (no repository-specific override)",
value: "",
},
...applicableRepos.map((repo) => ({ name: repo, value: repo })),
];
const selectedRepo = await Select.prompt({
message: "Multiple repository overrides found. Select which to use:",
options: choices
options: choices,
});
if (selectedRepo) {
@@ -128,9 +130,15 @@ async function resolveEffectiveSyncOptions(
);
} else {
// Non-interactive mode - list options and use top-level
log.warn(`Multiple repository overrides found: ${applicableRepos.join(', ')}`);
log.warn(`Running in non-interactive mode. Use --repository flag to specify which one to use.`);
log.info(`Falling back to top-level settings (no repository-specific overrides applied)`);
log.warn(
`Multiple repository overrides found: ${applicableRepos.join(", ")}`
);
log.warn(
`Running in non-interactive mode. Use --repository flag to specify which one to use.`
);
log.info(
`Falling back to top-level settings (no repository-specific overrides applied)`
);
}
}
}
@@ -280,6 +288,7 @@ export async function FSFSElement(
}
function prioritizeName(name: string): string {
if (name == "version") return "aaa";
if (name == "id") return "aa";
if (name == "type") return "ab";
if (name == "summary") return "ad";
@@ -292,6 +301,7 @@ function prioritizeName(name: string): string {
if (name == "failure_module") return "ak";
if (name == "input_transforms") return "al";
if (name == "lock") return "az";
if (name == "locks") return "azz";
return name;
}
@@ -310,9 +320,7 @@ export interface InlineScript {
content: string;
}
export function extractInlineScriptsForApps(
rec: any,
): InlineScript[] {
export function extractInlineScriptsForApps(rec: any): InlineScript[] {
if (!rec) {
return [];
}
@@ -390,7 +398,11 @@ function ZipFSElement(
async *getChildren(): AsyncIterable<DynFSElement> {
if (kind == "flow") {
const flow: OpenFlow = JSON.parse(await f.async("text"));
const inlineScripts = extractInlineScriptsForFlows(flow.value.modules, {}, SEP);
const inlineScripts = extractInlineScriptsForFlows(
flow.value.modules,
{},
SEP
);
for (const s of inlineScripts) {
yield {
isDirectory: false,
@@ -1020,7 +1032,8 @@ export async function ignoreF(wmillconf: {
wmillconf.includes?.some((i) => minimatch(file, i))) &&
(!wmillconf?.excludes ||
wmillconf.excludes!.every((i) => !minimatch(file, i))) &&
(!wmillconf.extraIncludes || wmillconf.extraIncludes.length === 0 ||
(!wmillconf.extraIncludes ||
wmillconf.extraIncludes.length === 0 ||
wmillconf.extraIncludes.some((i) => minimatch(file, i)))
);
},
@@ -1122,7 +1135,9 @@ async function buildTracker(changes: Change[]) {
return tracker;
}
export async function pull(opts: GlobalOptions & SyncOptions & { repository?: string }) {
export async function pull(
opts: GlobalOptions & SyncOptions & { repository?: string }
) {
if (opts.stateful) {
await ensureDir(path.join(Deno.cwd(), ".wmill"));
}
@@ -1131,7 +1146,10 @@ export async function pull(opts: GlobalOptions & SyncOptions & { repository?: st
await requireLogin(opts);
// Resolve effective sync options with repository awareness
const effectiveOpts = await resolveEffectiveSyncOptions(workspace, opts.repository);
const effectiveOpts = await resolveEffectiveSyncOptions(
workspace,
opts.repository
);
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
opts = mergeCliWithEffectiveOptions(opts, effectiveOpts);
@@ -1195,12 +1213,14 @@ export async function pull(opts: GlobalOptions & SyncOptions & { repository?: st
if (opts.dryRun && opts.jsonOutput) {
const result = {
success: true,
changes: changes.map(change => ({
changes: changes.map((change) => ({
type: change.name,
path: change.path,
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
...(change.name === "edited" && change.codebase
? { codebase_changed: true }
: {}),
})),
total: changes.length
total: changes.length,
};
console.log(JSON.stringify(result, null, 2));
return;
@@ -1361,12 +1381,14 @@ export async function pull(opts: GlobalOptions & SyncOptions & { repository?: st
const result = {
success: true,
message: `All ${changes.length} changes applied locally and wmill-lock.yaml updated`,
changes: changes.map(change => ({
changes: changes.map((change) => ({
type: change.name,
path: change.path,
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
...(change.name === "edited" && change.codebase
? { codebase_changed: true }
: {}),
})),
total: changes.length
total: changes.length,
};
console.log(JSON.stringify(result, null, 2));
} else {
@@ -1377,7 +1399,13 @@ export async function pull(opts: GlobalOptions & SyncOptions & { repository?: st
);
}
} else if (opts.jsonOutput) {
console.log(JSON.stringify({ success: true, message: "No changes to apply", total: 0 }, null, 2));
console.log(
JSON.stringify(
{ success: true, message: "No changes to apply", total: 0 },
null,
2
)
);
}
}
@@ -1432,12 +1460,17 @@ function removeSuffix(str: string, suffix: string) {
return str.slice(0, str.length - suffix.length);
}
export async function push(opts: GlobalOptions & SyncOptions & { repository?: string }) {
export async function push(
opts: GlobalOptions & SyncOptions & { repository?: string }
) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
// Resolve effective sync options with repository awareness
const effectiveOpts = await resolveEffectiveSyncOptions(workspace, opts.repository);
const effectiveOpts = await resolveEffectiveSyncOptions(
workspace,
opts.repository
);
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
opts = mergeCliWithEffectiveOptions(opts, effectiveOpts);
@@ -1574,12 +1607,14 @@ export async function push(opts: GlobalOptions & SyncOptions & { repository?: st
if (opts.dryRun && opts.jsonOutput) {
const result = {
success: true,
changes: changes.map(change => ({
changes: changes.map((change) => ({
type: change.name,
path: change.path,
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
...(change.name === "edited" && change.codebase
? { codebase_changed: true }
: {}),
})),
total: changes.length
total: changes.length,
};
console.log(JSON.stringify(result, null, 2));
return;
@@ -1953,26 +1988,38 @@ export async function push(opts: GlobalOptions & SyncOptions & { repository?: st
const result = {
success: true,
message: `All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name}`,
changes: changes.map(change => ({
changes: changes.map((change) => ({
type: change.name,
path: change.path,
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
...(change.name === "edited" && change.codebase
? { codebase_changed: true }
: {}),
})),
total: changes.length,
duration_ms: Math.round(performance.now() - start)
duration_ms: Math.round(performance.now() - start),
};
console.log(JSON.stringify(result, null, 2));
} else {
log.info(
colors.bold.green.underline(
`\nDone! All ${changes.length} changes pushed to the remote workspace ${
`\nDone! All ${
changes.length
} changes pushed to the remote workspace ${
workspace.workspaceId
} named ${workspace.name} (${(performance.now() - start).toFixed(0)}ms)`
} named ${workspace.name} (${(performance.now() - start).toFixed(
0
)}ms)`
)
);
}
} else if (opts.jsonOutput) {
console.log(JSON.stringify({ success: true, message: "No changes to push", total: 0 }, null, 2));
console.log(
JSON.stringify(
{ success: true, message: "No changes to push", total: 0 },
null,
2
)
);
}
}
@@ -14,7 +14,7 @@ interface InlineScript {
/**
* Extracts inline scripts from flow modules, converting them to separate files
* and replacing the original content with file references.
*
*
* @param modules - Array of flow modules to process
* @param mapping - Optional mapping of module IDs to custom file paths
* @param defaultTs - Default TypeScript runtime to use ("bun" or "deno")
@@ -28,11 +28,7 @@ export function extractInlineScripts(
): InlineScript[] {
return modules.flatMap((m) => {
if (m.value.type == "rawscript") {
const [basePath, ext] = assignPath(
m.id,
m.value.language,
defaultTs
);
const [basePath, ext] = assignPath(m.id, m.value.language, defaultTs);
const path = mapping[m.id] ?? basePath + ext;
const content = m.value.content;
const r = [{ path: path, content: content }];
@@ -45,13 +41,23 @@ export function extractInlineScripts(
}
return r;
} else if (m.value.type == "forloopflow") {
return extractInlineScripts(m.value.modules, mapping, separator, defaultTs);
return extractInlineScripts(
m.value.modules,
mapping,
separator,
defaultTs
);
} else if (m.value.type == "branchall") {
return m.value.branches.flatMap((b) =>
extractInlineScripts(b.modules, mapping, separator, defaultTs)
);
} else if (m.value.type == "whileloopflow") {
return extractInlineScripts(m.value.modules, mapping, separator, defaultTs);
return extractInlineScripts(
m.value.modules,
mapping,
separator,
defaultTs
);
} else if (m.value.type == "branchone") {
return [
...m.value.branches.flatMap((b) =>
@@ -68,7 +74,7 @@ export function extractInlineScripts(
/**
* Extracts the current mapping of module IDs to file paths from flow modules
* by analyzing existing inline script references.
*
*
* @param modules - Array of flow modules to analyze (can be undefined)
* @param mapping - Existing mapping to extend (defaults to empty object)
* @returns Record mapping module IDs to their corresponding file paths
@@ -80,12 +86,12 @@ export function extractCurrentMapping(
if (!modules || !Array.isArray(modules)) {
return mapping;
}
modules.forEach((m) => {
if (!m?.value?.type) {
return;
}
if (m.value.type === "rawscript") {
if (m.value.content && m.value.content.startsWith("!inline ")) {
mapping[m.id] = m.value.content.trim().split(" ")[1];
@@ -96,12 +102,16 @@ export function extractCurrentMapping(
) {
extractCurrentMapping(m.value.modules, mapping);
} else if (m.value.type === "branchall") {
m.value.branches.forEach((b) => extractCurrentMapping(b.modules, mapping));
m.value.branches.forEach((b) =>
extractCurrentMapping(b.modules, mapping)
);
} else if (m.value.type === "branchone") {
m.value.branches.forEach((b) => extractCurrentMapping(b.modules, mapping));
m.value.branches.forEach((b) =>
extractCurrentMapping(b.modules, mapping)
);
extractCurrentMapping(m.value.default, mapping);
}
});
return mapping;
}
}