From ca8a6274bc81ad49fa0c6166694ae4d65a4048cb Mon Sep 17 00:00:00 2001 From: wendrul <53628737+wendrul@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:15:00 +0100 Subject: [PATCH 01/25] fix: use !inline ref for scripts inside flows (preproc, error, ai tool) (#8319) * fix: use !inline ref for scripts inside flows (preproc, error, ai tool) * add test * path assign better --- cli/src/commands/flow/flow.ts | 18 +- cli/src/commands/flow/flow_metadata.ts | 30 +- cli/src/commands/sync/sync.ts | 23 +- ...nline_scripts_failure_preprocessor.test.ts | 498 ++++++++++++++++++ .../src/inline-scripts/extractor.ts | 11 +- 5 files changed, 573 insertions(+), 7 deletions(-) create mode 100644 cli/test/inline_scripts_failure_preprocessor.test.ts diff --git a/cli/src/commands/flow/flow.ts b/cli/src/commands/flow/flow.ts index eff742ee68..f0974cf354 100644 --- a/cli/src/commands/flow/flow.ts +++ b/cli/src/commands/flow/flow.ts @@ -56,13 +56,20 @@ export async function pushFlow( } const localFlow = (await yamlParseFile(localPath + "flow.yaml")) as FlowFile; + const fileReader = async (path: string) => await readFile(localPath + path, "utf-8"); await replaceInlineScripts( localFlow.value.modules, - async (path: string) => await readFile(localPath + path, "utf-8"), + fileReader, log, localPath, SEP ); + if (localFlow.value.failure_module) { + await replaceInlineScripts([localFlow.value.failure_module], fileReader, log, localPath, SEP); + } + if (localFlow.value.preprocessor_module) { + await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, log, localPath, SEP); + } if (flow) { if (isSuperset(localFlow, flow)) { @@ -252,13 +259,20 @@ async function preview( const localFlow = (await yamlParseFile(flowPath + "flow.yaml")) as FlowFile; // Replace inline scripts with their actual content + const fileReader = async (path: string) => await readFile(flowPath + path, "utf-8"); await replaceInlineScripts( localFlow.value.modules, - async (path: string) => await readFile(flowPath + path, "utf-8"), + fileReader, log, flowPath, SEP ); + if (localFlow.value.failure_module) { + await replaceInlineScripts([localFlow.value.failure_module], fileReader, log, flowPath, SEP); + } + if (localFlow.value.preprocessor_module) { + await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, log, flowPath, SEP); + } const input = opts.data ? await resolve(opts.data) : {}; diff --git a/cli/src/commands/flow/flow_metadata.ts b/cli/src/commands/flow/flow_metadata.ts index cb2e5336e6..d52d05a3ed 100644 --- a/cli/src/commands/flow/flow_metadata.ts +++ b/cli/src/commands/flow/flow_metadata.ts @@ -19,6 +19,7 @@ import { } from "../../utils/metadata.ts"; import { ScriptLanguage } from "../../utils/script_common.ts"; import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts"; +import { newPathAssigner } from "../../../windmill-utils-internal/src/path-utils/path-assigner.ts"; import { generateHash, getHeaders, writeIfChanged } from "../../utils/utils.ts"; import { exts } from "../script/script.ts"; @@ -121,14 +122,21 @@ export async function generateFlowLockInternal( } log.info(`Recomputing locks of ${changedScripts.join(", ")} in ${folder}`); + const fileReader = async (path: string) => await readFile(folder + SEP + path, "utf-8"); await replaceInlineScripts( flowValue.value.modules, - async (path: string) => await readFile(folder + SEP + path, "utf-8"), + fileReader, log, folder + SEP!, SEP, changedScripts ); + if (flowValue.value.failure_module) { + await replaceInlineScripts([flowValue.value.failure_module], fileReader, log, folder + SEP!, SEP, changedScripts); + } + if (flowValue.value.preprocessor_module) { + await replaceInlineScripts([flowValue.value.preprocessor_module], fileReader, log, folder + SEP!, SEP, changedScripts); + } //removeChangedLocks flowValue.value = await updateFlow( @@ -138,12 +146,20 @@ export async function generateFlowLockInternal( filteredDeps ); + const lockAssigner = newPathAssigner(opts.defaultTs ?? "bun"); const inlineScripts = extractInlineScriptsForFlows( flowValue.value.modules, {}, SEP, - opts.defaultTs + opts.defaultTs, + lockAssigner ); + if (flowValue.value.failure_module) { + inlineScripts.push(...extractInlineScriptsForFlows([flowValue.value.failure_module], {}, SEP, opts.defaultTs, lockAssigner)); + } + if (flowValue.value.preprocessor_module) { + inlineScripts.push(...extractInlineScriptsForFlows([flowValue.value.preprocessor_module], {}, SEP, opts.defaultTs, lockAssigner)); + } inlineScripts.forEach((s) => { writeIfChanged(process.cwd() + SEP + folder + SEP + s.path, s.content); }); @@ -176,7 +192,15 @@ async function filterWorkspaceDependenciesForFlow( rawWorkspaceDependencies: Record, folder: string ): Promise> { - const inlineScripts = extractInlineScriptsForFlows(structuredClone(flowValue.modules), {}, SEP, undefined); + const clonedValue = structuredClone(flowValue); + const depAssigner = newPathAssigner("bun"); + const inlineScripts = extractInlineScriptsForFlows(clonedValue.modules, {}, SEP, undefined, depAssigner); + if (clonedValue.failure_module) { + inlineScripts.push(...extractInlineScriptsForFlows([clonedValue.failure_module], {}, SEP, undefined, depAssigner)); + } + if (clonedValue.preprocessor_module) { + inlineScripts.push(...extractInlineScriptsForFlows([clonedValue.preprocessor_module], {}, SEP, undefined, depAssigner)); + } // Filter out lock files and map to common interface const scripts = inlineScripts diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index 7d48a14848..6375a55ada 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -592,14 +592,35 @@ function ZipFSElement( } let inlineScripts; try { + const assigner = newPathAssigner(defaultTs, { skipInlineScriptSuffix: getNonDottedPaths() }); inlineScripts = extractInlineScriptsForFlows( flow.value.modules as any, {}, SEP, defaultTs, - undefined, // pathAssigner - let it create one + assigner, { skipInlineScriptSuffix: getNonDottedPaths() }, ); + if (flow.value.failure_module) { + inlineScripts.push(...extractInlineScriptsForFlows( + [flow.value.failure_module], + {}, + SEP, + defaultTs, + assigner, + { skipInlineScriptSuffix: getNonDottedPaths() }, + )); + } + if (flow.value.preprocessor_module) { + inlineScripts.push(...extractInlineScriptsForFlows( + [flow.value.preprocessor_module], + {}, + SEP, + defaultTs, + assigner, + { skipInlineScriptSuffix: getNonDottedPaths() }, + )); + } } catch (error) { log.error( `Failed to extract inline scripts for flow at path: ${p}`, diff --git a/cli/test/inline_scripts_failure_preprocessor.test.ts b/cli/test/inline_scripts_failure_preprocessor.test.ts new file mode 100644 index 0000000000..1a9150a257 --- /dev/null +++ b/cli/test/inline_scripts_failure_preprocessor.test.ts @@ -0,0 +1,498 @@ +/** + * Unit tests for failure_module and preprocessor_module inline script + * extraction (pull) and replacement (push). + * + * These tests verify that rawscript content in failure_module and + * preprocessor_module is correctly extracted to !inline references + * and resolved back, matching the existing behavior for regular modules. + */ + +import { expect, test, describe } from "bun:test"; +import { extractInlineScripts, extractCurrentMapping } from "../windmill-utils-internal/src/inline-scripts/extractor.ts"; +import { replaceInlineScripts } from "../windmill-utils-internal/src/inline-scripts/replacer.ts"; +import { newPathAssigner } from "../windmill-utils-internal/src/path-utils/path-assigner.ts"; +import type { FlowModule } from "../windmill-utils-internal/src/gen/types.gen.ts"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeRawscriptModule( + id: string, + content: string, + language: "bun" | "python3" | "deno" = "bun", + lock?: string, +): FlowModule { + return { + id, + value: { + type: "rawscript" as const, + content, + language, + lock: lock, + input_transforms: {}, + }, + }; +} + +const noopLogger = { + info: () => {}, + error: () => {}, +}; + +// --------------------------------------------------------------------------- +// extractInlineScripts — PULL direction +// --------------------------------------------------------------------------- + +describe("extractInlineScripts for failure_module / preprocessor_module", () => { + test("extracts rawscript from failure_module wrapped in array", () => { + const failureModule = makeRawscriptModule( + "failure", + 'export function main() { throw new Error("handler"); }', + "bun", + ); + + const scripts = extractInlineScripts([failureModule], {}, "/", "bun"); + + expect(scripts.length).toBeGreaterThanOrEqual(1); + const script = scripts.find((s) => !s.is_lock); + expect(script).toBeDefined(); + expect(script!.content).toBe( + 'export function main() { throw new Error("handler"); }', + ); + // The module content should have been replaced with an !inline reference + expect(failureModule.value.content).toStartWith("!inline "); + }); + + test("extracts rawscript from preprocessor_module wrapped in array", () => { + const preprocessorModule = makeRawscriptModule( + "preprocessor", + "export function main() { return {}; }", + "python3", + ); + + const scripts = extractInlineScripts( + [preprocessorModule], + {}, + "/", + "bun", + ); + + expect(scripts.length).toBeGreaterThanOrEqual(1); + const script = scripts.find((s) => !s.is_lock); + expect(script).toBeDefined(); + expect(script!.content).toBe("export function main() { return {}; }"); + expect(script!.language).toBe("python3"); + expect(preprocessorModule.value.content).toStartWith("!inline "); + }); + + test("extracts lock alongside content", () => { + const mod = makeRawscriptModule( + "failure", + "console.log('hi')", + "bun", + "some-lock-content", + ); + + const scripts = extractInlineScripts([mod], {}, "/", "bun"); + + const contentScript = scripts.find((s) => !s.is_lock); + const lockScript = scripts.find((s) => s.is_lock); + expect(contentScript).toBeDefined(); + expect(lockScript).toBeDefined(); + expect(lockScript!.content).toBe("some-lock-content"); + expect((mod.value as any).lock).toStartWith("!inline "); + }); + + test("shared pathAssigner prevents collisions when summaries match", () => { + // If a regular module and failure_module share the same summary, + // a shared PathAssigner deduplicates via its internal counter. + const regular = makeRawscriptModule("a", "code_a", "bun"); + regular.summary = "my step"; + const failure = makeRawscriptModule("failure", "code_failure", "bun"); + failure.summary = "my step"; // same summary — would collide without shared assigner + + const assigner = newPathAssigner("bun"); + const scripts1 = extractInlineScripts([regular], {}, "/", "bun", assigner); + const scripts2 = extractInlineScripts([failure], {}, "/", "bun", assigner); + + const allPaths = [...scripts1, ...scripts2] + .filter((s) => !s.is_lock) + .map((s) => s.path); + + // All paths should be unique despite identical summaries + expect(allPaths.length).toBe(2); + expect(new Set(allPaths).size).toBe(2); + }); + + test("without shared pathAssigner, identical summaries produce duplicate paths", () => { + // Demonstrates the problem that sharing a PathAssigner solves. + const regular = makeRawscriptModule("a", "code_a", "bun"); + regular.summary = "my step"; + const failure = makeRawscriptModule("failure", "code_failure", "bun"); + failure.summary = "my step"; + + // Separate assigners — each starts with a fresh counter + const scripts1 = extractInlineScripts([regular], {}, "/", "bun"); + const scripts2 = extractInlineScripts([failure], {}, "/", "bun"); + + const allPaths = [...scripts1, ...scripts2] + .filter((s) => !s.is_lock) + .map((s) => s.path); + + // Without a shared assigner, the paths collide + expect(allPaths.length).toBe(2); + expect(new Set(allPaths).size).toBe(1); // both got the same path + }); + + test("skips non-rawscript failure_module (identity type)", () => { + const identityModule: FlowModule = { + id: "failure", + value: { type: "identity" as any }, + }; + const scripts = extractInlineScripts([identityModule], {}, "/", "bun"); + expect(scripts).toEqual([]); + }); +}); + +// --------------------------------------------------------------------------- +// replaceInlineScripts — PUSH direction +// --------------------------------------------------------------------------- + +describe("replaceInlineScripts for failure_module / preprocessor_module", () => { + test("resolves !inline reference back to file content", async () => { + const failureModule = makeRawscriptModule( + "failure", + "!inline failure.inline_script.ts", + "bun", + ); + + const files: Record = { + "failure.inline_script.ts": 'export function main() { return "error handled"; }', + }; + + await replaceInlineScripts( + [failureModule], + async (path) => { + if (!(path in files)) throw new Error(`File not found: ${path}`); + return files[path]; + }, + noopLogger, + "/tmp/test/", + "/", + ); + + expect(failureModule.value.content).toBe( + 'export function main() { return "error handled"; }', + ); + }); + + test("resolves !inline reference for preprocessor_module", async () => { + const preprocessorModule = makeRawscriptModule( + "preprocessor", + "!inline preprocessor.inline_script.py", + "python3", + ); + + const files: Record = { + "preprocessor.inline_script.py": "def main(): return {}", + }; + + await replaceInlineScripts( + [preprocessorModule], + async (path) => { + if (!(path in files)) throw new Error(`File not found: ${path}`); + return files[path]; + }, + noopLogger, + "/tmp/test/", + "/", + ); + + expect(preprocessorModule.value.content).toBe("def main(): return {}"); + }); + + test("resolves !inline lock reference", async () => { + const mod = makeRawscriptModule( + "failure", + "!inline failure.inline_script.ts", + "bun", + "!inline failure.inline_script.lock", + ); + + const files: Record = { + "failure.inline_script.ts": "code here", + "failure.inline_script.lock": "lock-data-here", + }; + + await replaceInlineScripts( + [mod], + async (path) => { + if (!(path in files)) throw new Error(`File not found: ${path}`); + return files[path]; + }, + noopLogger, + "/tmp/test/", + "/", + ); + + expect(mod.value.content).toBe("code here"); + expect((mod.value as any).lock).toBe("lock-data-here"); + }); + + test("leaves non-inline content untouched", async () => { + const mod = makeRawscriptModule( + "failure", + "export function main() { return 1; }", + "bun", + ); + + await replaceInlineScripts( + [mod], + async () => { + throw new Error("should not be called"); + }, + noopLogger, + "/tmp/test/", + "/", + ); + + expect(mod.value.content).toBe("export function main() { return 1; }"); + }); +}); + +// --------------------------------------------------------------------------- +// Round-trip: extract then replace +// --------------------------------------------------------------------------- + +describe("round-trip extract → replace for failure_module / preprocessor_module", () => { + test("failure_module content survives extract + replace", async () => { + const originalContent = 'export function main(error: any) {\n console.error(error);\n return { handled: true };\n}'; + const failureModule = makeRawscriptModule( + "failure", + originalContent, + "bun", + ); + + // PULL: extract inline scripts (mutates module in place) + const extracted = extractInlineScripts([failureModule], {}, "/", "bun"); + expect(failureModule.value.content).toStartWith("!inline "); + + // Build a virtual filesystem from extracted scripts + const files: Record = {}; + for (const s of extracted) { + files[s.path] = s.content; + } + + // PUSH: replace inline references back + await replaceInlineScripts( + [failureModule], + async (path) => { + if (!(path in files)) throw new Error(`File not found: ${path}`); + return files[path]; + }, + noopLogger, + "/tmp/test/", + "/", + ); + + expect(failureModule.value.content).toBe(originalContent); + }); + + test("preprocessor_module content survives extract + replace", async () => { + const originalContent = "def main():\n return {\"preprocessed\": True}"; + const preprocessorModule = makeRawscriptModule( + "preprocessor", + originalContent, + "python3", + ); + + const extracted = extractInlineScripts( + [preprocessorModule], + {}, + "/", + "bun", + ); + expect(preprocessorModule.value.content).toStartWith("!inline "); + + const files: Record = {}; + for (const s of extracted) { + files[s.path] = s.content; + } + + await replaceInlineScripts( + [preprocessorModule], + async (path) => { + if (!(path in files)) throw new Error(`File not found: ${path}`); + return files[path]; + }, + noopLogger, + "/tmp/test/", + "/", + ); + + expect(preprocessorModule.value.content).toBe(originalContent); + }); + + test("failure_module with lock survives extract + replace", async () => { + const originalContent = "export function main() { return 42; }"; + const originalLock = "package-lock-contents-here"; + const mod = makeRawscriptModule( + "failure", + originalContent, + "bun", + originalLock, + ); + + const extracted = extractInlineScripts([mod], {}, "/", "bun"); + + const files: Record = {}; + for (const s of extracted) { + files[s.path] = s.content; + } + + await replaceInlineScripts( + [mod], + async (path) => { + if (!(path in files)) throw new Error(`File not found: ${path}`); + return files[path]; + }, + noopLogger, + "/tmp/test/", + "/", + ); + + expect(mod.value.content).toBe(originalContent); + expect((mod.value as any).lock).toBe(originalLock); + }); + + test("full flow with modules + failure_module + preprocessor_module round-trips", async () => { + const regularContent = "export function main() { return 'step1'; }"; + const failureContent = "export function main(e: any) { return e; }"; + const preprocessorContent = "def main():\n pass"; + + const modules = [makeRawscriptModule("a", regularContent, "bun")]; + const failureModule = makeRawscriptModule("failure", failureContent, "bun"); + const preprocessorModule = makeRawscriptModule("preprocessor", preprocessorContent, "python3"); + + // Extract all (mimicking sync.ts pull logic) + const allExtracted = [ + ...extractInlineScripts(modules, {}, "/", "bun"), + ...extractInlineScripts([failureModule], {}, "/", "bun"), + ...extractInlineScripts([preprocessorModule], {}, "/", "bun"), + ]; + + // All modules should now have !inline references + expect(modules[0].value.content).toStartWith("!inline "); + expect(failureModule.value.content).toStartWith("!inline "); + expect(preprocessorModule.value.content).toStartWith("!inline "); + + // All paths should be unique + const paths = allExtracted.filter((s) => !s.is_lock).map((s) => s.path); + expect(new Set(paths).size).toBe(paths.length); + + // Build filesystem + const files: Record = {}; + for (const s of allExtracted) { + files[s.path] = s.content; + } + + const fileReader = async (path: string) => { + if (!(path in files)) throw new Error(`File not found: ${path}`); + return files[path]; + }; + + // Replace all (mimicking flow.ts push logic) + await replaceInlineScripts(modules, fileReader, noopLogger, "/tmp/", "/"); + await replaceInlineScripts([failureModule], fileReader, noopLogger, "/tmp/", "/"); + await replaceInlineScripts([preprocessorModule], fileReader, noopLogger, "/tmp/", "/"); + + expect(modules[0].value.content).toBe(regularContent); + expect(failureModule.value.content).toBe(failureContent); + expect(preprocessorModule.value.content).toBe(preprocessorContent); + }); +}); + +// --------------------------------------------------------------------------- +// extractCurrentMapping +// --------------------------------------------------------------------------- + +describe("extractCurrentMapping for failure_module / preprocessor_module", () => { + test("extracts mapping from failure_module via optional param", () => { + const failureModule: FlowModule = makeRawscriptModule( + "failure", + "!inline failure.inline_script.ts", + "bun", + ); + + const mapping = extractCurrentMapping( + undefined, + {}, + failureModule, + undefined, + ); + + expect(mapping["failure"]).toBe("failure.inline_script.ts"); + }); + + test("extracts mapping from preprocessor_module via optional param", () => { + const preprocessorModule: FlowModule = makeRawscriptModule( + "preprocessor", + "!inline preprocessor.inline_script.py", + "python3", + ); + + const mapping = extractCurrentMapping( + undefined, + {}, + undefined, + preprocessorModule, + ); + + expect(mapping["preprocessor"]).toBe("preprocessor.inline_script.py"); + }); + + test("extracts mapping from modules + failure + preprocessor combined", () => { + const modules: FlowModule[] = [ + makeRawscriptModule("a", "!inline a.inline_script.ts", "bun"), + ]; + const failureModule = makeRawscriptModule( + "failure", + "!inline failure.inline_script.ts", + "bun", + ); + const preprocessorModule = makeRawscriptModule( + "preprocessor", + "!inline preprocessor.inline_script.py", + "python3", + ); + + const mapping = extractCurrentMapping( + modules, + {}, + failureModule, + preprocessorModule, + ); + + expect(mapping["a"]).toBe("a.inline_script.ts"); + expect(mapping["failure"]).toBe("failure.inline_script.ts"); + expect(mapping["preprocessor"]).toBe("preprocessor.inline_script.py"); + }); + + test("ignores non-inline content in failure_module", () => { + const failureModule = makeRawscriptModule( + "failure", + "export function main() {}", + "bun", + ); + + const mapping = extractCurrentMapping( + undefined, + {}, + failureModule, + undefined, + ); + + expect(mapping["failure"]).toBeUndefined(); + }); +}); diff --git a/cli/windmill-utils-internal/src/inline-scripts/extractor.ts b/cli/windmill-utils-internal/src/inline-scripts/extractor.ts index df9a1db08b..a3572ce7eb 100644 --- a/cli/windmill-utils-internal/src/inline-scripts/extractor.ts +++ b/cli/windmill-utils-internal/src/inline-scripts/extractor.ts @@ -150,8 +150,17 @@ export function extractInlineScripts( */ export function extractCurrentMapping( modules: FlowModule[] | undefined, - mapping: Record = {} + mapping: Record = {}, + failureModule?: FlowModule, + preprocessorModule?: FlowModule, ): Record { + if (failureModule) { + extractCurrentMapping([failureModule], mapping); + } + if (preprocessorModule) { + extractCurrentMapping([preprocessorModule], mapping); + } + if (!modules || !Array.isArray(modules)) { return mapping; } From 7fb729cc8483a2e6966a8e8995678929f4d451a0 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:18:03 +0100 Subject: [PATCH 02/25] fix(cli): instruct agent to tell user about generate-metadata and sync push instead of running them (#8318) Co-authored-by: Claude Opus 4.5 --- cli/src/guidance/skills.ts | 101 +++++++++++++----- .../auto-generated/cli/cli-commands.md | 2 +- system_prompts/auto-generated/flow.md | 4 +- system_prompts/auto-generated/prompts.ts | 6 +- .../schemas/kafka_trigger.schema.yaml | 7 ++ .../skills/cli-commands/SKILL.md | 2 +- .../auto-generated/skills/raw-app/SKILL.md | 6 +- .../auto-generated/skills/resources/SKILL.md | 2 +- .../auto-generated/skills/schedules/SKILL.md | 2 + .../auto-generated/skills/triggers/SKILL.md | 2 + .../auto-generated/skills/write-flow/SKILL.md | 4 +- .../skills/write-script-bash/SKILL.md | 4 +- .../skills/write-script-bigquery/SKILL.md | 4 +- .../skills/write-script-bun/SKILL.md | 4 +- .../skills/write-script-bunnative/SKILL.md | 4 +- .../skills/write-script-csharp/SKILL.md | 4 +- .../skills/write-script-deno/SKILL.md | 4 +- .../skills/write-script-duckdb/SKILL.md | 4 +- .../skills/write-script-go/SKILL.md | 4 +- .../skills/write-script-graphql/SKILL.md | 4 +- .../skills/write-script-java/SKILL.md | 4 +- .../skills/write-script-mssql/SKILL.md | 4 +- .../skills/write-script-mysql/SKILL.md | 4 +- .../skills/write-script-nativets/SKILL.md | 4 +- .../skills/write-script-php/SKILL.md | 4 +- .../skills/write-script-postgresql/SKILL.md | 4 +- .../skills/write-script-powershell/SKILL.md | 4 +- .../skills/write-script-python3/SKILL.md | 4 +- .../skills/write-script-rust/SKILL.md | 4 +- .../skills/write-script-snowflake/SKILL.md | 4 +- system_prompts/base/flow-base.md | 4 +- system_prompts/base/raw-app.md | 6 +- system_prompts/base/resources.md | 2 +- system_prompts/base/schedules.md | 2 + system_prompts/base/triggers.md | 2 + system_prompts/generate.py | 4 +- 36 files changed, 177 insertions(+), 57 deletions(-) diff --git a/cli/src/guidance/skills.ts b/cli/src/guidance/skills.ts index 34e288c7d7..e8abfdd924 100644 --- a/cli/src/guidance/skills.ts +++ b/cli/src/guidance/skills.ts @@ -43,10 +43,12 @@ description: MUST use when writing Go scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # Go @@ -115,10 +117,12 @@ description: MUST use when writing Java scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # Java @@ -167,10 +171,12 @@ description: MUST use when writing GraphQL queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # GraphQL @@ -226,10 +232,12 @@ description: MUST use when writing Rust scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # Rust @@ -315,10 +323,12 @@ description: MUST use when writing Bun Native scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # TypeScript (Bun Native) @@ -967,10 +977,12 @@ description: MUST use when writing PostgreSQL queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # PostgreSQL @@ -992,10 +1004,12 @@ description: MUST use when writing PHP scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # PHP @@ -1063,10 +1077,12 @@ description: MUST use when writing BigQuery queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # BigQuery @@ -1088,10 +1104,12 @@ description: MUST use when writing Bun/TypeScript scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # TypeScript (Bun) @@ -1742,10 +1760,12 @@ description: MUST use when writing C# scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # C# @@ -1797,10 +1817,12 @@ description: MUST use when writing MS SQL Server queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # Microsoft SQL Server (MSSQL) @@ -1822,10 +1844,12 @@ description: MUST use when writing Deno/TypeScript scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # TypeScript (Deno) @@ -2480,10 +2504,12 @@ description: MUST use when writing MySQL queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # MySQL @@ -2505,10 +2531,12 @@ description: MUST use when writing PowerShell scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # PowerShell @@ -2574,10 +2602,12 @@ description: MUST use when writing Snowflake queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # Snowflake @@ -2599,10 +2629,12 @@ description: MUST use when writing Python scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # Python @@ -3404,10 +3436,12 @@ description: MUST use when writing DuckDB queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # DuckDB @@ -3469,10 +3503,12 @@ description: MUST use when writing Bash scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # Bash @@ -3532,10 +3568,12 @@ description: MUST use when writing Native TypeScript scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - \`wmill script generate-metadata\` - Generate .script.yaml and .lock files - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use \`wmill resource-type list --schema\` to discover available resource types. # TypeScript (Native) @@ -4155,10 +4193,12 @@ description: MUST use when creating flows. Create a folder ending with \`.flow\` and add a YAML file with the flow definition. For rawscript modules, use \`!inline path/to/script.ts\` for the content key. -After writing: +After writing, tell the user they can run: - \`wmill flow generate-locks --yes\` - Generate lock files for the specific flow you modified (e.g. \`wmill flow generate-locks f/my_folder/my_flow.flow --yes\`) - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + ## OpenFlow Schema The OpenFlow schema (openflow.openapi.yaml) is the source of truth for flow structure. Refer to OPENFLOW_SCHEMA for the complete type definitions. @@ -4356,7 +4396,7 @@ export async function main(user_id: string) { } \`\`\` -After creating, generate lock files: +After creating, tell the user they can generate lock files by running: \`\`\`bash wmill app generate-locks \`\`\` @@ -4509,6 +4549,8 @@ data: ## CLI Commands +Tell the user they can run these commands (do NOT run them yourself): + | Command | Description | |---------|-------------| | \`wmill app new\` | Create a new raw app interactively | @@ -4525,7 +4567,7 @@ data: 3. **Keep runnables focused** - one function per file 4. **Use descriptive IDs** - \`get_user.ts\` not \`a.ts\` 5. **Always whitelist tables** - add to \`data.tables\` before querying -6. **Generate locks** - run \`wmill app generate-locks\` after adding/modifying backend runnables +6. **Generate locks** - tell the user to run \`wmill app generate-locks\` after adding/modifying backend runnables `, "triggers": `--- name: triggers @@ -4547,6 +4589,8 @@ Examples: ## CLI Commands +After writing, tell the user they can run these commands (do NOT run them yourself): + \`\`\`bash # Push trigger configuration wmill sync push @@ -4596,6 +4640,8 @@ Windmill uses 6-field cron expressions (includes seconds): ## CLI Commands +After writing, tell the user they can run these commands (do NOT run them yourself): + \`\`\`bash # Push schedules to Windmill wmill sync push @@ -4851,7 +4897,7 @@ wmill resource-type list --schema # Get specific resource type schema wmill resource-type get postgresql -# Push resources +# Push resources (tell the user to run this, do NOT run it yourself) wmill sync push \`\`\` `, @@ -4864,7 +4910,7 @@ description: MUST use when using the CLI. The Windmill CLI (\`wmill\`) provides commands for managing scripts, flows, apps, and other resources. -Current version: 1.651.1 +Current version: 1.653.0 ## Global Options @@ -5613,6 +5659,13 @@ properties: key: type: string value: {} + auto_offset_reset: + type: string + enum: + - latest + - earliest + description: Initial offset behavior when consumer group has no committed offset. + 'latest' starts from new messages only, 'earliest' starts from the beginning. error_handler_path: type: string description: Path to a script or flow to run when the triggered job fails diff --git a/system_prompts/auto-generated/cli/cli-commands.md b/system_prompts/auto-generated/cli/cli-commands.md index ce94c1fe28..9d80c0d52f 100644 --- a/system_prompts/auto-generated/cli/cli-commands.md +++ b/system_prompts/auto-generated/cli/cli-commands.md @@ -2,7 +2,7 @@ The Windmill CLI (`wmill`) provides commands for managing scripts, flows, apps, and other resources. -Current version: 1.651.1 +Current version: 1.653.0 ## Global Options diff --git a/system_prompts/auto-generated/flow.md b/system_prompts/auto-generated/flow.md index 6adbe267ef..8b00ad0a0c 100644 --- a/system_prompts/auto-generated/flow.md +++ b/system_prompts/auto-generated/flow.md @@ -4,10 +4,12 @@ Create a folder ending with `.flow` and add a YAML file with the flow definition. For rawscript modules, use `!inline path/to/script.ts` for the content key. -After writing: +After writing, tell the user they can run: - `wmill flow generate-locks --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow.flow --yes`) - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + ## OpenFlow Schema The OpenFlow schema (openflow.openapi.yaml) is the source of truth for flow structure. Refer to OPENFLOW_SCHEMA for the complete type definitions. diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index 6192d42931..e68374fa7f 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -35,10 +35,12 @@ export const FLOW_BASE = `# Windmill Flow Building Guide Create a folder ending with \`.flow\` and add a YAML file with the flow definition. For rawscript modules, use \`!inline path/to/script.ts\` for the content key. -After writing: +After writing, tell the user they can run: - \`wmill flow generate-locks --yes\` - Generate lock files for the specific flow you modified (e.g. \`wmill flow generate-locks f/my_folder/my_flow.flow --yes\`) - \`wmill sync push\` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + ## OpenFlow Schema The OpenFlow schema (openflow.openapi.yaml) is the source of truth for flow structure. Refer to OPENFLOW_SCHEMA for the complete type definitions. @@ -1356,7 +1358,7 @@ export const CLI_COMMANDS = `# Windmill CLI Commands The Windmill CLI (\`wmill\`) provides commands for managing scripts, flows, apps, and other resources. -Current version: 1.651.1 +Current version: 1.653.0 ## Global Options diff --git a/system_prompts/auto-generated/schemas/kafka_trigger.schema.yaml b/system_prompts/auto-generated/schemas/kafka_trigger.schema.yaml index 1a0c98ef41..74cfa8e615 100644 --- a/system_prompts/auto-generated/schemas/kafka_trigger.schema.yaml +++ b/system_prompts/auto-generated/schemas/kafka_trigger.schema.yaml @@ -25,6 +25,13 @@ properties: key: type: string value: {} + auto_offset_reset: + type: string + enum: + - latest + - earliest + description: Initial offset behavior when consumer group has no committed offset. + 'latest' starts from new messages only, 'earliest' starts from the beginning. error_handler_path: type: string description: Path to a script or flow to run when the triggered job fails diff --git a/system_prompts/auto-generated/skills/cli-commands/SKILL.md b/system_prompts/auto-generated/skills/cli-commands/SKILL.md index 66d8d51b4a..562d0b8536 100644 --- a/system_prompts/auto-generated/skills/cli-commands/SKILL.md +++ b/system_prompts/auto-generated/skills/cli-commands/SKILL.md @@ -7,7 +7,7 @@ description: MUST use when using the CLI. The Windmill CLI (`wmill`) provides commands for managing scripts, flows, apps, and other resources. -Current version: 1.651.1 +Current version: 1.653.0 ## Global Options diff --git a/system_prompts/auto-generated/skills/raw-app/SKILL.md b/system_prompts/auto-generated/skills/raw-app/SKILL.md index 8fb05e43c9..533e5f7c3e 100644 --- a/system_prompts/auto-generated/skills/raw-app/SKILL.md +++ b/system_prompts/auto-generated/skills/raw-app/SKILL.md @@ -84,7 +84,7 @@ export async function main(user_id: string) { } ``` -After creating, generate lock files: +After creating, tell the user they can generate lock files by running: ```bash wmill app generate-locks ``` @@ -237,6 +237,8 @@ data: ## CLI Commands +Tell the user they can run these commands (do NOT run them yourself): + | Command | Description | |---------|-------------| | `wmill app new` | Create a new raw app interactively | @@ -253,4 +255,4 @@ data: 3. **Keep runnables focused** - one function per file 4. **Use descriptive IDs** - `get_user.ts` not `a.ts` 5. **Always whitelist tables** - add to `data.tables` before querying -6. **Generate locks** - run `wmill app generate-locks` after adding/modifying backend runnables +6. **Generate locks** - tell the user to run `wmill app generate-locks` after adding/modifying backend runnables diff --git a/system_prompts/auto-generated/skills/resources/SKILL.md b/system_prompts/auto-generated/skills/resources/SKILL.md index 649cb39cbb..3f78cc1b0b 100644 --- a/system_prompts/auto-generated/skills/resources/SKILL.md +++ b/system_prompts/auto-generated/skills/resources/SKILL.md @@ -242,6 +242,6 @@ wmill resource-type list --schema # Get specific resource type schema wmill resource-type get postgresql -# Push resources +# Push resources (tell the user to run this, do NOT run it yourself) wmill sync push ``` diff --git a/system_prompts/auto-generated/skills/schedules/SKILL.md b/system_prompts/auto-generated/skills/schedules/SKILL.md index 1cd64cb002..24dab471e5 100644 --- a/system_prompts/auto-generated/skills/schedules/SKILL.md +++ b/system_prompts/auto-generated/skills/schedules/SKILL.md @@ -39,6 +39,8 @@ Windmill uses 6-field cron expressions (includes seconds): ## CLI Commands +After writing, tell the user they can run these commands (do NOT run them yourself): + ```bash # Push schedules to Windmill wmill sync push diff --git a/system_prompts/auto-generated/skills/triggers/SKILL.md b/system_prompts/auto-generated/skills/triggers/SKILL.md index 5f3a532051..183f447175 100644 --- a/system_prompts/auto-generated/skills/triggers/SKILL.md +++ b/system_prompts/auto-generated/skills/triggers/SKILL.md @@ -18,6 +18,8 @@ Examples: ## CLI Commands +After writing, tell the user they can run these commands (do NOT run them yourself): + ```bash # Push trigger configuration wmill sync push diff --git a/system_prompts/auto-generated/skills/write-flow/SKILL.md b/system_prompts/auto-generated/skills/write-flow/SKILL.md index e4a8bc976c..f844b813bd 100644 --- a/system_prompts/auto-generated/skills/write-flow/SKILL.md +++ b/system_prompts/auto-generated/skills/write-flow/SKILL.md @@ -9,10 +9,12 @@ description: MUST use when creating flows. Create a folder ending with `.flow` and add a YAML file with the flow definition. For rawscript modules, use `!inline path/to/script.ts` for the content key. -After writing: +After writing, tell the user they can run: - `wmill flow generate-locks --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow.flow --yes`) - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + ## OpenFlow Schema The OpenFlow schema (openflow.openapi.yaml) is the source of truth for flow structure. Refer to OPENFLOW_SCHEMA for the complete type definitions. diff --git a/system_prompts/auto-generated/skills/write-script-bash/SKILL.md b/system_prompts/auto-generated/skills/write-script-bash/SKILL.md index a914902deb..b776538001 100644 --- a/system_prompts/auto-generated/skills/write-script-bash/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-bash/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing Bash scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # Bash diff --git a/system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md b/system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md index 8561d3170e..cd3c9e1610 100644 --- a/system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing BigQuery queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # BigQuery diff --git a/system_prompts/auto-generated/skills/write-script-bun/SKILL.md b/system_prompts/auto-generated/skills/write-script-bun/SKILL.md index 3758a172e2..6957a139f9 100644 --- a/system_prompts/auto-generated/skills/write-script-bun/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-bun/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing Bun/TypeScript scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # TypeScript (Bun) diff --git a/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md b/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md index 0ae5b57474..731462addd 100644 --- a/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing Bun Native scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # TypeScript (Bun Native) diff --git a/system_prompts/auto-generated/skills/write-script-csharp/SKILL.md b/system_prompts/auto-generated/skills/write-script-csharp/SKILL.md index e0d268d55e..ca807520e0 100644 --- a/system_prompts/auto-generated/skills/write-script-csharp/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-csharp/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing C# scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # C# diff --git a/system_prompts/auto-generated/skills/write-script-deno/SKILL.md b/system_prompts/auto-generated/skills/write-script-deno/SKILL.md index a23c8ceccd..a925e9835a 100644 --- a/system_prompts/auto-generated/skills/write-script-deno/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-deno/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing Deno/TypeScript scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # TypeScript (Deno) diff --git a/system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md b/system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md index 04f6a3fdec..1df6392db9 100644 --- a/system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing DuckDB queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # DuckDB diff --git a/system_prompts/auto-generated/skills/write-script-go/SKILL.md b/system_prompts/auto-generated/skills/write-script-go/SKILL.md index ff6b1c490c..894a1dd791 100644 --- a/system_prompts/auto-generated/skills/write-script-go/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-go/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing Go scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # Go diff --git a/system_prompts/auto-generated/skills/write-script-graphql/SKILL.md b/system_prompts/auto-generated/skills/write-script-graphql/SKILL.md index 452a1d4734..0749cc47ef 100644 --- a/system_prompts/auto-generated/skills/write-script-graphql/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-graphql/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing GraphQL queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # GraphQL diff --git a/system_prompts/auto-generated/skills/write-script-java/SKILL.md b/system_prompts/auto-generated/skills/write-script-java/SKILL.md index facc50899e..811fa875ef 100644 --- a/system_prompts/auto-generated/skills/write-script-java/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-java/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing Java scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # Java diff --git a/system_prompts/auto-generated/skills/write-script-mssql/SKILL.md b/system_prompts/auto-generated/skills/write-script-mssql/SKILL.md index 58ea4982a2..f6bc5e008a 100644 --- a/system_prompts/auto-generated/skills/write-script-mssql/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-mssql/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing MS SQL Server queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # Microsoft SQL Server (MSSQL) diff --git a/system_prompts/auto-generated/skills/write-script-mysql/SKILL.md b/system_prompts/auto-generated/skills/write-script-mysql/SKILL.md index 8028fa6f1c..28ba025931 100644 --- a/system_prompts/auto-generated/skills/write-script-mysql/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-mysql/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing MySQL queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # MySQL diff --git a/system_prompts/auto-generated/skills/write-script-nativets/SKILL.md b/system_prompts/auto-generated/skills/write-script-nativets/SKILL.md index 18eebcbc74..24d2c14440 100644 --- a/system_prompts/auto-generated/skills/write-script-nativets/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-nativets/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing Native TypeScript scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # TypeScript (Native) diff --git a/system_prompts/auto-generated/skills/write-script-php/SKILL.md b/system_prompts/auto-generated/skills/write-script-php/SKILL.md index c2d02ff3bf..a8d6b2b0ab 100644 --- a/system_prompts/auto-generated/skills/write-script-php/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-php/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing PHP scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # PHP diff --git a/system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md b/system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md index df1fc6b5ca..ccb4654fff 100644 --- a/system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing PostgreSQL queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # PostgreSQL diff --git a/system_prompts/auto-generated/skills/write-script-powershell/SKILL.md b/system_prompts/auto-generated/skills/write-script-powershell/SKILL.md index e54f3e647b..fefc379b12 100644 --- a/system_prompts/auto-generated/skills/write-script-powershell/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-powershell/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing PowerShell scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # PowerShell diff --git a/system_prompts/auto-generated/skills/write-script-python3/SKILL.md b/system_prompts/auto-generated/skills/write-script-python3/SKILL.md index 15e459c241..5d6007df7a 100644 --- a/system_prompts/auto-generated/skills/write-script-python3/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-python3/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing Python scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # Python diff --git a/system_prompts/auto-generated/skills/write-script-rust/SKILL.md b/system_prompts/auto-generated/skills/write-script-rust/SKILL.md index 3dad884ebd..044cb9059a 100644 --- a/system_prompts/auto-generated/skills/write-script-rust/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-rust/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing Rust scripts. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # Rust diff --git a/system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md b/system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md index 54667c0432..24b8d06d4f 100644 --- a/system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md @@ -5,10 +5,12 @@ description: MUST use when writing Snowflake queries. ## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types. # Snowflake diff --git a/system_prompts/base/flow-base.md b/system_prompts/base/flow-base.md index 513617693e..55d4c06b58 100644 --- a/system_prompts/base/flow-base.md +++ b/system_prompts/base/flow-base.md @@ -4,10 +4,12 @@ Create a folder ending with `.flow` and add a YAML file with the flow definition. For rawscript modules, use `!inline path/to/script.ts` for the content key. -After writing: +After writing, tell the user they can run: - `wmill flow generate-locks --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow.flow --yes`) - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + ## OpenFlow Schema The OpenFlow schema (openflow.openapi.yaml) is the source of truth for flow structure. Refer to OPENFLOW_SCHEMA for the complete type definitions. diff --git a/system_prompts/base/raw-app.md b/system_prompts/base/raw-app.md index 0dbcac9462..5d68232eda 100644 --- a/system_prompts/base/raw-app.md +++ b/system_prompts/base/raw-app.md @@ -79,7 +79,7 @@ export async function main(user_id: string) { } ``` -After creating, generate lock files: +After creating, tell the user they can generate lock files by running: ```bash wmill app generate-locks ``` @@ -232,6 +232,8 @@ data: ## CLI Commands +Tell the user they can run these commands (do NOT run them yourself): + | Command | Description | |---------|-------------| | `wmill app new` | Create a new raw app interactively | @@ -248,4 +250,4 @@ data: 3. **Keep runnables focused** - one function per file 4. **Use descriptive IDs** - `get_user.ts` not `a.ts` 5. **Always whitelist tables** - add to `data.tables` before querying -6. **Generate locks** - run `wmill app generate-locks` after adding/modifying backend runnables +6. **Generate locks** - tell the user to run `wmill app generate-locks` after adding/modifying backend runnables diff --git a/system_prompts/base/resources.md b/system_prompts/base/resources.md index 290d6f617b..0f51f6d322 100644 --- a/system_prompts/base/resources.md +++ b/system_prompts/base/resources.md @@ -237,6 +237,6 @@ wmill resource-type list --schema # Get specific resource type schema wmill resource-type get postgresql -# Push resources +# Push resources (tell the user to run this, do NOT run it yourself) wmill sync push ``` diff --git a/system_prompts/base/schedules.md b/system_prompts/base/schedules.md index bf14d24cbd..8e50fb87a6 100644 --- a/system_prompts/base/schedules.md +++ b/system_prompts/base/schedules.md @@ -34,6 +34,8 @@ Windmill uses 6-field cron expressions (includes seconds): ## CLI Commands +After writing, tell the user they can run these commands (do NOT run them yourself): + ```bash # Push schedules to Windmill wmill sync push diff --git a/system_prompts/base/triggers.md b/system_prompts/base/triggers.md index 4998b85342..5205eb2d4f 100644 --- a/system_prompts/base/triggers.md +++ b/system_prompts/base/triggers.md @@ -13,6 +13,8 @@ Examples: ## CLI Commands +After writing, tell the user they can run these commands (do NOT run them yourself): + ```bash # Push trigger configuration wmill sync push diff --git a/system_prompts/generate.py b/system_prompts/generate.py index 0af7fe0eb6..122b85e61a 100644 --- a/system_prompts/generate.py +++ b/system_prompts/generate.py @@ -763,10 +763,12 @@ def generate_skills( # CLI intro for script skills script_cli_intro = """## CLI Commands -Place scripts in a folder. After writing, run: +Place scripts in a folder. After writing, tell the user they can run: - `wmill script generate-metadata` - Generate .script.yaml and .lock files - `wmill sync push` - Deploy to Windmill +Do NOT run these commands yourself. Instead, inform the user that they should run them. + Use `wmill resource-type list --schema` to discover available resource types.""" skills_generated = [] From f3e9a29c13ac9420febccd0c1c4e7d5474f03a89 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 12 Mar 2026 12:28:17 +0000 Subject: [PATCH 03/25] add customUi props for History and Save to workspace editor bar buttons (#8336) Co-authored-by: Claude Opus 4.6 --- frontend/src/lib/components/EditorBar.svelte | 4 ++-- frontend/src/lib/components/custom_ui.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index cdaec4c44c..4ddbb70b19 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -1096,7 +1096,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
{@render right?.()} - {#if scriptPath && !noHistory} + {#if scriptPath && !noHistory && customUi?.history != false}
{#if !emptyString(repo.git_repo_resource_path)} diff --git a/frontend/src/lib/hub.ts b/frontend/src/lib/hub.ts index fcaa2ab832..a038f18580 100644 --- a/frontend/src/lib/hub.ts +++ b/frontend/src/lib/hub.ts @@ -92,7 +92,6 @@ export function rawAppToHubUrl(hubBaseUrl: string, summary?: string): URL { } type HubPaths = { - gitSync: string gitSyncTest: string gitInitRepo: string slackErrorHandler: string diff --git a/frontend/src/lib/hubPaths.json b/frontend/src/lib/hubPaths.json index 79bf362003..d62fffa097 100644 --- a/frontend/src/lib/hubPaths.json +++ b/frontend/src/lib/hubPaths.json @@ -1,28 +1,28 @@ { - "gitSync_0": "hub/9087/sync-script-to-git-repo-windmill", - "gitSync_1": "hub/9987/sync-script-to-git-repo-windmill", - "gitSync_2": "hub/11498/sync-script-to-git-repo-windmill", - "gitSync_3": "hub/11533/sync-script-to-git-repo-windmill", - "gitSync_4": "hub/11580/sync-script-to-git-repo-windmill", - "gitSync_5": "hub/11641/sync-script-to-git-repo-windmill", - "gitSync_6": "hub/11666/sync-script-to-git-repo-windmill", - "gitSync_7": "hub/11668/sync-script-to-git-repo-windmill", - "gitSync_8": "hub/19673/sync-script-to-git-repo-windmill", - "gitSync_9": "hub/19738/sync-script-to-git-repo-windmill", - "gitSync_10": "hub/19785/sync-script-to-git-repo-windmill", - "gitSync_11": "hub/19789/sync-script-to-git-repo-windmill", - "gitSync_12": "hub/19798/sync-script-to-git-repo-windmill", - "gitSync_13": "hub/19801/sync-script-to-git-repo-windmill", - "gitSync_14": "hub/19803/sync-script-to-git-repo-windmill", - "gitSync_15": "hub/19816/sync-script-to-git-repo-windmill", - "gitSync_16": "hub/19818/sync-script-to-git-repo-windmill", - "gitSync_17": "hub/28073/sync-script-to-git-repo-windmill", - "gitSync_18": "hub/28078/sync-script-to-git-repo-windmill", - "gitSync_19": "hub/28081/sync-script-to-git-repo-windmill", - "gitSync_20": "hub/28102/sync-script-to-git-repo-windmill", - "gitSync_21": "hub/28131/sync-script-to-git-repo-windmill", - "gitSync_22": "hub/28159/sync-script-to-git-repo-windmill", - "gitSync": "hub/28160/sync-script-to-git-repo-windmill", + "deprecated_gitSync_0": "hub/9087/sync-script-to-git-repo-windmill", + "deprecated_gitSync_1": "hub/9987/sync-script-to-git-repo-windmill", + "deprecated_gitSync_2": "hub/11498/sync-script-to-git-repo-windmill", + "deprecated_gitSync_3": "hub/11533/sync-script-to-git-repo-windmill", + "deprecated_gitSync_4": "hub/11580/sync-script-to-git-repo-windmill", + "deprecated_gitSync_5": "hub/11641/sync-script-to-git-repo-windmill", + "deprecated_gitSync_6": "hub/11666/sync-script-to-git-repo-windmill", + "deprecated_gitSync_7": "hub/11668/sync-script-to-git-repo-windmill", + "deprecated_gitSync_8": "hub/19673/sync-script-to-git-repo-windmill", + "deprecated_gitSync_9": "hub/19738/sync-script-to-git-repo-windmill", + "deprecated_gitSync_10": "hub/19785/sync-script-to-git-repo-windmill", + "deprecated_gitSync_11": "hub/19789/sync-script-to-git-repo-windmill", + "deprecated_gitSync_12": "hub/19798/sync-script-to-git-repo-windmill", + "deprecated_gitSync_13": "hub/19801/sync-script-to-git-repo-windmill", + "deprecated_gitSync_14": "hub/19803/sync-script-to-git-repo-windmill", + "deprecated_gitSync_15": "hub/19816/sync-script-to-git-repo-windmill", + "deprecated_gitSync_16": "hub/19818/sync-script-to-git-repo-windmill", + "deprecated_gitSync_17": "hub/28073/sync-script-to-git-repo-windmill", + "deprecated_gitSync_18": "hub/28078/sync-script-to-git-repo-windmill", + "deprecated_gitSync_19": "hub/28081/sync-script-to-git-repo-windmill", + "deprecated_gitSync_20": "hub/28102/sync-script-to-git-repo-windmill", + "deprecated_gitSync_21": "hub/28131/sync-script-to-git-repo-windmill", + "deprecated_gitSync_22": "hub/28159/sync-script-to-git-repo-windmill", + "deprecated_gitSync_latest": "hub/28160/sync-script-to-git-repo-windmill", "gitSyncTest": "hub/19799/git-repo-test-read-write-windmill", "gitInitRepo_0": "hub/28134/git-sync%3A-init-repository-windmill", "gitInitRepo": "hub/28158/git-sync%3A-init-repository-windmill", diff --git a/integration_tests/requirements.txt b/integration_tests/requirements.txt index 97572f1b84..35b4f13cf4 100644 --- a/integration_tests/requirements.txt +++ b/integration_tests/requirements.txt @@ -6,3 +6,4 @@ httpx==0.26.0 idna==3.6 sniffio==1.3.0 docker==7.1.0 +gitpython==3.1.43 diff --git a/integration_tests/test/git_sync_test.py b/integration_tests/test/git_sync_test.py new file mode 100644 index 0000000000..5268fefa01 --- /dev/null +++ b/integration_tests/test/git_sync_test.py @@ -0,0 +1,792 @@ +import os +import shutil +import tempfile +import time +import unittest +import uuid + +import git as gitpython + +from .wmill_integration_test_utils import WindmillClient, GiteaClient + + +# Script content template for bun/TypeScript scripts +def ts_script(body: str) -> str: + return f"export async function main() {{\n {body}\n}}\n" + + +def unique_name(prefix: str = "git-sync-test") -> str: + return f"{prefix}-{uuid.uuid4().hex[:8]}" + + +class TestGitSync(unittest.TestCase): + _client: WindmillClient + _gitea: GiteaClient + _repos_to_cleanup: list + _fork_workspaces_to_cleanup: list + + @classmethod + def setUpClass(cls) -> None: + print("Running {}".format(cls.__name__)) + cls._client = WindmillClient() + cls._gitea = GiteaClient() + cls._gitea.setup_admin() + cls._repos_to_cleanup = [] + cls._fork_workspaces_to_cleanup = [] + + @classmethod + def tearDownClass(cls) -> None: + # Disable git sync to avoid interfering with other tests + try: + cls._client.configure_git_sync({"repositories": []}) + except Exception as e: + print(f"Warning: failed to disable git sync: {e}") + + for fork_id in cls._fork_workspaces_to_cleanup: + try: + cls._client.delete_workspace(fork_id) + except Exception as e: + print(f"Warning: failed to delete fork workspace {fork_id}: {e}") + + for repo_name in cls._repos_to_cleanup: + cls._gitea.delete_repo(repo_name) + + def setUp(self): + """Wait for any pending deployment callbacks from previous tests to drain.""" + time.sleep(2) + # Wait until no new deployment callback jobs appear for 4 seconds + prev_count = self._client.count_deployment_callback_jobs() + for _ in range(3): + time.sleep(2) + cur_count = self._client.count_deployment_callback_jobs() + if cur_count == prev_count: + break + prev_count = cur_count + + def _create_test_repo(self) -> tuple: + """Create a Gitea repo and return (repo_name, docker_clone_url).""" + name = unique_name() + docker_url = self._gitea.create_repo(name) + self._repos_to_cleanup.append(name) + return name, docker_url + + def _setup_git_sync_resource(self, repo_name: str, branch: str = "main") -> str: + """Create a git_repository resource pointing to the Gitea repo. + Returns the resource path.""" + resource_path = f"u/admin/git_sync_{repo_name.replace('-', '_')}" + docker_url = self._gitea.get_docker_clone_url(repo_name) + self._client.create_resource( + path=resource_path, + resource_type="git_repository", + value={ + "url": docker_url, + "branch": branch, + "is_github_app": False, + }, + update_if_exists=True, + ) + return resource_path + + def _configure_single_repo_sync( + self, + resource_path: str, + include_type=None, + include_path=None, + use_individual_branch=False, + group_by_folder=False, + force_branch=None, + ): + """Configure git sync with a single repository (auto-managed script).""" + repo_settings = { + "git_repo_resource_path": f"$res:{resource_path}", + "use_individual_branch": use_individual_branch, + "group_by_folder": group_by_folder, + } + if force_branch: + repo_settings["force_branch"] = force_branch + if include_type or include_path: + repo_settings["settings"] = { + "include_type": include_type or [], + "include_path": include_path if include_path is not None else ["**"], + } + + self._client.configure_git_sync({ + "repositories": [repo_settings], + }) + + def _clone_repo(self, repo_name: str, branch: str = None) -> str: + """Clone the repo to a temp dir and return the path.""" + host_url = self._gitea.get_host_clone_url(repo_name) + tmp_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, tmp_dir, ignore_errors=True) + args = {} + if branch: + args["branch"] = branch + gitpython.Repo.clone_from(host_url, tmp_dir, **args) + return tmp_dir + + def _clone_repo_all_branches(self, repo_name: str) -> str: + """Clone the repo fetching all branches.""" + host_url = self._gitea.get_host_clone_url(repo_name) + tmp_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, tmp_dir, ignore_errors=True) + gitpython.Repo.clone_from(host_url, tmp_dir, no_single_branch=True) + return tmp_dir + + def _list_repo_files(self, repo_dir: str, branch: str = None) -> list: + """List all tracked files in the repo (relative paths).""" + repo = gitpython.Repo(repo_dir) + if branch: + commit = repo.refs[branch].commit + else: + commit = repo.head.commit + return [item.path for item in commit.tree.traverse()] + + def _read_file_content(self, repo_dir: str, file_path: str) -> str: + """Read a file's content from the repo working tree.""" + full_path = os.path.join(repo_dir, file_path) + with open(full_path, "r") as f: + return f.read() + + def _get_commit_count(self, repo_dir: str, branch: str = "main") -> int: + repo = gitpython.Repo(repo_dir) + return len(list(repo.iter_commits(branch))) + + def _get_last_commit_message(self, repo_dir: str, branch: str = "main") -> str: + repo = gitpython.Repo(repo_dir) + return repo.iter_commits(branch).__next__().message + + def _get_branches(self, repo_dir: str) -> list: + repo = gitpython.Repo(repo_dir) + return [ref.name for ref in repo.remote().refs] + + def _create_folder(self, folder_name: str): + """Create a folder in the workspace, ignoring errors if it already exists.""" + try: + self._client._client.post( + f"/api/w/{self._client._workspace}/folders/create", + json={"name": folder_name}, + ) + except Exception: + pass + + # ────────────────────────────────────────────────── + # Core happy-path tests + # ────────────────────────────────────────────────── + + def test_script_deploy_syncs_to_git(self): + """Deploy a script and verify it appears in the git repo with correct content.""" + repo_name, _ = self._create_test_repo() + resource_path = self._setup_git_sync_resource(repo_name) + self._configure_single_repo_sync( + resource_path, + include_type=["script"], + ) + + initial_count = self._client.count_deployment_callback_jobs() + + script_path = f"u/admin/{unique_name('sync_test')}" + self._client.create_script( + path=script_path, + content=ts_script("return 42"), + language="bun", + ) + + self._client.wait_for_sync_jobs(initial_count, min_new=1) + time.sleep(3) + + repo_dir = self._clone_repo(repo_name) + files = self._list_repo_files(repo_dir) + + # The script should appear in the repo + matching = [f for f in files if script_path in f] + self.assertTrue( + len(matching) > 0, + f"Expected script '{script_path}' in repo files: {files}", + ) + + # Verify file content matches what we deployed + script_file = [f for f in matching if f.endswith(".ts")][0] + content = self._read_file_content(repo_dir, script_file) + self.assertIn( + "return 42", + content, + f"Expected 'return 42' in script content: {content}", + ) + + def test_multi_repo_routing(self): + """Two repos with different path filters receive the correct objects.""" + repo_name_a, _ = self._create_test_repo() + repo_name_b, _ = self._create_test_repo() + res_path_a = self._setup_git_sync_resource(repo_name_a) + res_path_b = self._setup_git_sync_resource(repo_name_b) + + folder_a = unique_name("folder_a") + folder_b = unique_name("folder_b") + + self._client.configure_git_sync({ + "repositories": [ + { + "git_repo_resource_path": f"$res:{res_path_a}", + "use_individual_branch": False, + "group_by_folder": False, + "settings": { + "include_type": ["script"], + "include_path": [f"f/{folder_a}/**"], + }, + }, + { + "git_repo_resource_path": f"$res:{res_path_b}", + "use_individual_branch": False, + "group_by_folder": False, + "settings": { + "include_type": ["script"], + "include_path": [f"f/{folder_b}/**"], + }, + }, + ], + }) + + self._create_folder(folder_a) + self._create_folder(folder_b) + + initial_count = self._client.count_deployment_callback_jobs() + + script_a = f"f/{folder_a}/script_a" + script_b = f"f/{folder_b}/script_b" + + self._client.create_script( + path=script_a, + content=ts_script("return 'a'"), + language="bun", + ) + self._client.create_script( + path=script_b, + content=ts_script("return 'b'"), + language="bun", + ) + + # Wait for at least 2 deployment callback jobs + self._client.wait_for_sync_jobs(initial_count, min_new=2) + time.sleep(3) + + # Verify repo A has script_a but not script_b + repo_dir_a = self._clone_repo(repo_name_a) + files_a = self._list_repo_files(repo_dir_a) + self.assertTrue( + any("script_a" in f for f in files_a), + f"Expected script_a in repo A files: {files_a}", + ) + self.assertFalse( + any("script_b" in f for f in files_a), + f"Did not expect script_b in repo A files: {files_a}", + ) + + # Verify repo B has script_b but not script_a + repo_dir_b = self._clone_repo(repo_name_b) + files_b = self._list_repo_files(repo_dir_b) + self.assertTrue( + any("script_b" in f for f in files_b), + f"Expected script_b in repo B files: {files_b}", + ) + self.assertFalse( + any("script_a" in f for f in files_b), + f"Did not expect script_a in repo B files: {files_b}", + ) + + def test_script_update_creates_new_commit_with_updated_content(self): + """Updating a script should produce a new commit with the new content.""" + repo_name, _ = self._create_test_repo() + resource_path = self._setup_git_sync_resource(repo_name) + self._configure_single_repo_sync(resource_path, include_type=["script"]) + + script_path = f"u/admin/{unique_name('update_test')}" + + # Create initial script + initial_count = self._client.count_deployment_callback_jobs() + self._client.create_script( + path=script_path, + content=ts_script("return 1"), + language="bun", + ) + self._client.wait_for_sync_jobs(initial_count, min_new=1) + time.sleep(3) + + repo_dir = self._clone_repo(repo_name) + initial_commits = self._get_commit_count(repo_dir) + + # Update the script + update_count = self._client.count_deployment_callback_jobs() + self._client.update_script( + path=script_path, + content=ts_script("return 2"), + language="bun", + ) + self._client.wait_for_sync_jobs(update_count, min_new=1) + time.sleep(3) + + # Re-clone and check commit count increased + repo_dir2 = self._clone_repo(repo_name) + new_commits = self._get_commit_count(repo_dir2) + self.assertGreater( + new_commits, + initial_commits, + f"Expected more commits after update: {new_commits} vs {initial_commits}", + ) + + # Verify file content reflects the update + files = self._list_repo_files(repo_dir2) + script_file = [f for f in files if script_path in f and f.endswith(".ts")][0] + content = self._read_file_content(repo_dir2, script_file) + self.assertIn( + "return 2", + content, + f"Expected 'return 2' in updated script content: {content}", + ) + self.assertNotIn( + "return 1", + content, + f"Did not expect 'return 1' in updated script content: {content}", + ) + + def test_deploy_multiple_object_types(self): + """Deploy a script, flow, and variable and verify all appear in the repo.""" + repo_name, _ = self._create_test_repo() + resource_path = self._setup_git_sync_resource(repo_name) + self._configure_single_repo_sync( + resource_path, + include_type=["script", "flow", "variable"], + ) + + initial_count = self._client.count_deployment_callback_jobs() + + suffix = unique_name("multi") + script_path = f"u/admin/{suffix}_script" + flow_path = f"u/admin/{suffix}_flow" + var_path = f"u/admin/{suffix}_var" + + self._client.create_script( + path=script_path, + content=ts_script("return 'multi'"), + language="bun", + ) + self._client.create_flow( + path=flow_path, + flow_value_json="""{ + "summary": "test flow", + "value": { + "modules": [{ + "id": "a", + "value": { + "type": "rawscript", + "content": "export async function main() { return 1 }", + "language": "bun", + "input_transforms": {}, + "tag": "" + } + }] + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": {}, + "required": [], + "type": "object", + "order": [] + } + }""", + ) + self._client.create_variable( + path=var_path, + value="test_value", + ) + + # Wait for 3 deployment callbacks (one per object) + self._client.wait_for_sync_jobs(initial_count, min_new=3) + time.sleep(3) + + repo_dir = self._clone_repo(repo_name) + files = self._list_repo_files(repo_dir) + files_str = "\n".join(files) + + self.assertTrue( + any(suffix + "_script" in f for f in files), + f"Expected script in repo:\n{files_str}", + ) + self.assertTrue( + any(suffix + "_flow" in f for f in files), + f"Expected flow in repo:\n{files_str}", + ) + self.assertTrue( + any(suffix + "_var" in f for f in files), + f"Expected variable in repo:\n{files_str}", + ) + + # ────────────────────────────────────────────────── + # Commit message verification + # ────────────────────────────────────────────────── + + def test_commit_message_format(self): + """Verify commit messages have the [WM] prefix.""" + repo_name, _ = self._create_test_repo() + resource_path = self._setup_git_sync_resource(repo_name) + self._configure_single_repo_sync(resource_path, include_type=["script"]) + + initial_count = self._client.count_deployment_callback_jobs() + script_path = f"u/admin/{unique_name('commit_msg')}" + self._client.create_script( + path=script_path, + content=ts_script("return 'msg'"), + language="bun", + ) + self._client.wait_for_sync_jobs(initial_count, min_new=1) + time.sleep(3) + + repo_dir = self._clone_repo(repo_name) + commit_msg = self._get_last_commit_message(repo_dir) + + self.assertTrue( + commit_msg.startswith("[WM]"), + f"Expected commit message to start with '[WM]', got: {commit_msg!r}", + ) + + # ────────────────────────────────────────────────── + # Rename handling + # ────────────────────────────────────────────────── + + def test_rename_removes_old_file(self): + """Renaming a script should remove the old file and create the new one.""" + repo_name, _ = self._create_test_repo() + resource_path = self._setup_git_sync_resource(repo_name) + self._configure_single_repo_sync(resource_path, include_type=["script"]) + + old_path = f"u/admin/{unique_name('rename_old')}" + + # Create initial script + initial_count = self._client.count_deployment_callback_jobs() + self._client.create_script( + path=old_path, + content=ts_script("return 'old'"), + language="bun", + ) + self._client.wait_for_sync_jobs(initial_count, min_new=1) + time.sleep(3) + + # Verify old script exists in repo + repo_dir = self._clone_repo(repo_name) + files = self._list_repo_files(repo_dir) + old_name = old_path.split("/")[-1] + self.assertTrue( + any(old_name in f for f in files), + f"Expected old script '{old_name}' in repo: {files}", + ) + + # Create new script at different path (simulates rename) + new_path = f"u/admin/{unique_name('rename_new')}" + rename_count = self._client.count_deployment_callback_jobs() + self._client.create_script( + path=new_path, + content=ts_script("return 'renamed'"), + language="bun", + ) + # Also delete the old script + self._client.delete_script(old_path) + # Wait for both create and delete deployment callbacks + self._client.wait_for_sync_jobs(rename_count, min_new=2) + time.sleep(3) + + # Verify new script exists + repo_dir2 = self._clone_repo(repo_name) + files2 = self._list_repo_files(repo_dir2) + new_name = new_path.split("/")[-1] + self.assertTrue( + any(new_name in f for f in files2), + f"Expected new script '{new_name}' in repo: {files2}", + ) + self.assertFalse( + any(old_name in f for f in files2), + f"Expected old script '{old_name}' to be removed: {files2}", + ) + + # ────────────────────────────────────────────────── + # Promotion mode (individual branches) + # ────────────────────────────────────────────────── + + def test_promotion_mode_creates_per_object_branches(self): + """In promotion mode (use_individual_branch=True), each deploy creates + a branch named wm_deploy/{workspace}/{path_type}/{path} with the content + on that branch, not on main.""" + repo_name, _ = self._create_test_repo() + resource_path = self._setup_git_sync_resource(repo_name) + self._configure_single_repo_sync( + resource_path, + include_type=["script"], + use_individual_branch=True, + ) + + initial_count = self._client.count_deployment_callback_jobs() + script_path = f"u/admin/{unique_name('promo')}" + self._client.create_script( + path=script_path, + content=ts_script("return 'promotion'"), + language="bun", + ) + self._client.wait_for_sync_jobs(initial_count, min_new=1) + time.sleep(3) + + # Clone with all branches + repo_dir = self._clone_repo_all_branches(repo_name) + branches = self._get_branches(repo_dir) + + # Should have a branch matching wm_deploy pattern + wm_branches = [b for b in branches if "wm_deploy/" in b] + self.assertTrue( + len(wm_branches) > 0, + f"Expected wm_deploy/ branch, got branches: {branches}", + ) + + # The branch name should contain 'script' (the path_type) + deploy_branch = wm_branches[0] + self.assertIn( + "script", + deploy_branch, + f"Expected 'script' in branch name: {deploy_branch}", + ) + + # The script path (with / replaced by __) should appear in the branch name + script_name = script_path.split("/")[-1] + self.assertIn( + script_name, + deploy_branch.replace("/", "__"), + f"Expected script name '{script_name}' in branch: {deploy_branch}", + ) + + # Verify main branch does NOT have the script + main_files = self._list_repo_files(repo_dir, branch="origin/main") + self.assertFalse( + any(script_name in f for f in main_files), + f"Did not expect script on main branch, but found it: {main_files}", + ) + + # Verify the deploy branch HAS the script + local_branch_name = deploy_branch.replace("origin/", "") + repo = gitpython.Repo(repo_dir) + repo.git.checkout(local_branch_name) + branch_files = self._list_repo_files(repo_dir) + self.assertTrue( + any(script_name in f for f in branch_files), + f"Expected script on deploy branch '{local_branch_name}': {branch_files}", + ) + + def test_promotion_mode_group_by_folder(self): + """With use_individual_branch=True and group_by_folder=True, the branch name + uses the folder prefix (first 2 path segments joined by __) instead of the + full path.""" + repo_name, _ = self._create_test_repo() + resource_path = self._setup_git_sync_resource(repo_name) + self._configure_single_repo_sync( + resource_path, + include_type=["script"], + use_individual_branch=True, + group_by_folder=True, + ) + + folder_name = unique_name("grp") + self._create_folder(folder_name) + + initial_count = self._client.count_deployment_callback_jobs() + script_path = f"f/{folder_name}/{unique_name('grp_script')}" + self._client.create_script( + path=script_path, + content=ts_script("return 'grouped'"), + language="bun", + ) + self._client.wait_for_sync_jobs(initial_count, min_new=1) + time.sleep(3) + + repo_dir = self._clone_repo_all_branches(repo_name) + branches = self._get_branches(repo_dir) + + wm_branches = [b for b in branches if "wm_deploy/" in b] + self.assertTrue( + len(wm_branches) > 0, + f"Expected wm_deploy/ branch with group_by_folder: {branches}", + ) + + # With group_by_folder, the branch should contain the folder prefix + # format: wm_deploy/{workspace}/f__{folder_name} + deploy_branch = wm_branches[0] + expected_folder_part = f"f__{folder_name}" + self.assertIn( + expected_folder_part, + deploy_branch, + f"Expected folder-grouped branch name containing '{expected_folder_part}', got: {deploy_branch}", + ) + + # ────────────────────────────────────────────────── + # force_branch with wmill.yaml + # ────────────────────────────────────────────────── + + def test_force_branch_with_wmill_yaml(self): + """force_branch passes --branch to wmill sync pull, which selects the + matching gitBranches config from wmill.yaml. With branch-specific variables + configured, the variable file should use a branch-specific path.""" + repo_name, _ = self._create_test_repo() + resource_path = self._setup_git_sync_resource(repo_name) + + # Push a wmill.yaml to the repo that configures branch-specific variables + wmill_yaml_content = """\ +includes: + - "**" +gitBranches: + staging: + specificItems: + variables: + - "**" +""" + self._gitea.create_file(repo_name, "wmill.yaml", wmill_yaml_content) + + self._configure_single_repo_sync( + resource_path, + include_type=["variable"], + force_branch="staging", + ) + + initial_count = self._client.count_deployment_callback_jobs() + var_path = f"u/admin/{unique_name('env_var')}" + self._client.create_variable( + path=var_path, + value="staging_value", + ) + self._client.wait_for_sync_jobs(initial_count, min_new=1) + time.sleep(3) + + repo_dir = self._clone_repo(repo_name) + files = self._list_repo_files(repo_dir) + + var_name = var_path.split("/")[-1] + + # With force_branch="staging" and specificItems for variables, + # the variable file should have ".staging." in its name + staging_files = [f for f in files if var_name in f and ".staging." in f] + self.assertTrue( + len(staging_files) > 0, + f"Expected variable file with '.staging.' in name for branch-specific item, " + f"got files: {files}", + ) + + # ────────────────────────────────────────────────── + # Exclude path filtering + # ────────────────────────────────────────────────── + + def test_exclude_path_filtering(self): + """Scripts in excluded paths should not be synced to the repo.""" + repo_name, _ = self._create_test_repo() + resource_path = self._setup_git_sync_resource(repo_name) + + folder_inc = unique_name("inc") + folder_exc = unique_name("exc") + self._create_folder(folder_inc) + self._create_folder(folder_exc) + + self._client.configure_git_sync({ + "repositories": [{ + "git_repo_resource_path": f"$res:{resource_path}", + "use_individual_branch": False, + "group_by_folder": False, + "settings": { + "include_type": ["script"], + "include_path": ["f/**"], + "exclude_path": [f"f/{folder_exc}/**"], + }, + }], + }) + + initial_count = self._client.count_deployment_callback_jobs() + + script_inc = f"f/{folder_inc}/included_script" + script_exc = f"f/{folder_exc}/excluded_script" + + self._client.create_script( + path=script_inc, + content=ts_script("return 'included'"), + language="bun", + ) + self._client.create_script( + path=script_exc, + content=ts_script("return 'excluded'"), + language="bun", + ) + + # Only 1 sync job expected (the excluded one should not trigger) + self._client.wait_for_sync_jobs(initial_count, min_new=1) + time.sleep(5) + # Verify no extra sync jobs arrived for the excluded script + final_count = self._client.count_deployment_callback_jobs() + self.assertEqual( + final_count, initial_count + 1, + f"Expected exactly 1 new sync job, got {final_count - initial_count}", + ) + + repo_dir = self._clone_repo(repo_name) + files = self._list_repo_files(repo_dir) + + self.assertTrue( + any("included_script" in f for f in files), + f"Expected included_script in repo: {files}", + ) + self.assertFalse( + any("excluded_script" in f for f in files), + f"Did not expect excluded_script in repo: {files}", + ) + + # ────────────────────────────────────────────────── + # Workspace fork + # ────────────────────────────────────────────────── + + def test_workspace_fork_creates_branch(self): + """Forking a workspace with git sync configured should create a + fork branch in the git repo.""" + repo_name, _ = self._create_test_repo() + resource_path = self._setup_git_sync_resource(repo_name) + + # Configure git sync on the parent workspace (sync mode, not promotion) + self._configure_single_repo_sync( + resource_path, + include_type=["script"], + ) + + # Deploy a script first so there's content in the repo + initial_count = self._client.count_deployment_callback_jobs() + script_path = f"u/admin/{unique_name('fork_base')}" + self._client.create_script( + path=script_path, + content=ts_script("return 'base'"), + language="bun", + ) + self._client.wait_for_sync_jobs(initial_count, min_new=1) + time.sleep(3) + + # Create workspace fork + fork_id = f"wm-fork-{uuid.uuid4().hex[:8]}" + fork_name = f"Fork {fork_id}" + self._fork_workspaces_to_cleanup.append(fork_id) + + # Step 1: Create git branches for the fork + job_ids = self._client.create_workspace_fork_branch(fork_id, fork_name) + if job_ids: + self._client.wait_for_jobs_by_ids(job_ids, timeout=90) + time.sleep(3) + + # Step 2: Create the fork workspace + self._client.create_workspace_fork(fork_id, fork_name) + + # Verify a fork branch was created in the git repo + repo_dir = self._clone_repo_all_branches(repo_name) + branches = self._get_branches(repo_dir) + + # Fork branches are named: wm-fork/{original_branch}/{fork_id} + fork_branches = [b for b in branches if "wm-fork" in b] + self.assertTrue( + len(fork_branches) > 0, + f"Expected a wm-fork branch in the repo after forking, got: {branches}", + ) diff --git a/integration_tests/test/wmill_integration_test_utils.py b/integration_tests/test/wmill_integration_test_utils.py index 260986ef9b..93730f0d31 100644 --- a/integration_tests/test/wmill_integration_test_utils.py +++ b/integration_tests/test/wmill_integration_test_utils.py @@ -3,6 +3,7 @@ import time import httpx import json import os +import uuid @@ -13,9 +14,11 @@ class WindmillClient: _client: httpx.Client - def __init__(self): - self._workspace = "integration-tests" - self._url = "http://localhost:8000" + def __init__(self, workspace: str = "integration-tests", url: str = None): + if url is None: + url = os.environ.get("WINDMILL_BASE_URL", "http://localhost:8000") + self._workspace = workspace + self._url = url self._token = self._login() self._client = self._init_client() @@ -155,6 +158,55 @@ class WindmillClient: time.sleep(1) raise Exception(f"Script deployment failed for {path}") + def update_script(self, path: str, content: str, language: str, tag: str = None): + """Update an existing script by creating a new version with parent_hash.""" + print(f"Updating script {path}") + + # Get current script hash + response = self._client.get( + f"/api/w/{self._workspace}/scripts/get/p/{path}" + ) + if response.status_code // 100 != 2: + raise Exception(response.content.decode()) + current_hash = response.json()["hash"] + + payload = { + "path": path, + "content": content, + "description": "", + "summary": "", + "language": language, + "parent_hash": current_hash, + } + + if tag is not None: + payload["tag"] = tag + + response = self._client.post( + f"/api/w/{self._workspace}/scripts/create", + json=payload, + ) + if response.status_code // 100 != 2: + raise Exception(response.content.decode()) + script_hash = response.content.decode() + print(f"Script hash for path {path} is {script_hash}") + time_now = datetime.datetime.now(datetime.timezone.utc) + while datetime.datetime.now( + datetime.timezone.utc + ) - time_now < datetime.timedelta(seconds=60): + response = self._client.get( + f"/api/w/{self._workspace}/scripts/deployment_status/h/{script_hash}" + ) + if response.status_code // 100 != 2: + raise Exception(response.content.decode()) + elif response.json()["lock"] != None: + return + elif response.json()["lock_error_logs"] != None: + raise Exception(response.json()["lock_error_logs"]) + print(f"Waiting for script {path} with hash {script_hash} to be deployed") + time.sleep(1) + raise Exception(f"Script deployment failed for {path}") + def delete_script(self, path: str): print(f"Deleting script {path}") response = self._client.post( @@ -385,6 +437,120 @@ class WindmillClient: print(f"Exception when retrieving workers list: {e}") return [] + def create_resource(self, path: str, resource_type: str, value: dict, update_if_exists: bool = False): + print(f"Creating resource {path} of type {resource_type}") + params = {} + if update_if_exists: + params["update_if_exists"] = "true" + response = self._client.post( + f"/api/w/{self._workspace}/resources/create", + json={ + "path": path, + "value": value, + "resource_type": resource_type, + "description": "", + }, + params=params, + ) + if response.status_code // 100 != 2: + raise Exception(response.content.decode()) + return response.content.decode() + + def configure_git_sync(self, git_sync_settings: dict): + print(f"Configuring git sync with {len(git_sync_settings.get('repositories', []))} repositories") + response = self._client.post( + f"/api/w/{self._workspace}/workspaces/edit_git_sync_config", + json={"git_sync_settings": git_sync_settings}, + ) + if response.status_code // 100 != 2: + raise Exception(response.content.decode()) + return response.content.decode() + + def get_completed_jobs(self, job_kinds: str = None, success: bool = None): + params = {"per_page": 1000} + if job_kinds: + params["job_kinds"] = job_kinds + if success is not None: + params["success"] = str(success).lower() + response = self._client.get( + f"/api/w/{self._workspace}/jobs/completed/list", + params=params, + ) + if response.status_code // 100 != 2: + raise Exception(response.content.decode()) + return response.json() + + def wait_for_sync_jobs(self, initial_count: int, min_new: int = 1, timeout: int = 90) -> list: + """Poll completed DeploymentCallback jobs until count increases by min_new.""" + start = time.time() + current_count = initial_count + while time.time() - start < timeout: + jobs = self.get_completed_jobs(job_kinds="deploymentcallback") + current_count = len(jobs) + if current_count >= initial_count + min_new: + return jobs + time.sleep(2) + raise TimeoutError( + f"Timed out waiting for sync jobs: expected {initial_count + min_new}, " + f"got {current_count} after {timeout}s" + ) + + def count_deployment_callback_jobs(self) -> int: + jobs = self.get_completed_jobs(job_kinds="deploymentcallback") + return len(jobs) + + def create_workspace_fork_branch(self, fork_id: str, fork_name: str) -> list: + """Create git branches for a workspace fork. Returns list of job UUIDs to wait on.""" + print(f"Creating fork branch for {fork_id} from {self._workspace}") + response = self._client.post( + f"/api/w/{self._workspace}/workspaces/create_workspace_fork_branch", + json={"id": fork_id, "name": fork_name}, + ) + if response.status_code // 100 != 2: + raise Exception(response.content.decode()) + return response.json() + + def create_workspace_fork(self, fork_id: str, fork_name: str) -> str: + """Create a forked workspace (call after fork branch jobs complete).""" + print(f"Creating fork workspace {fork_id} from {self._workspace}") + response = self._client.post( + f"/api/w/{self._workspace}/workspaces/create_fork", + json={"id": fork_id, "name": fork_name}, + ) + if response.status_code // 100 != 2: + raise Exception(response.content.decode()) + return response.content.decode() + + def wait_for_jobs_by_ids(self, job_ids: list, timeout: int = 90): + """Wait for specific jobs (by UUID) to complete.""" + start = time.time() + while time.time() - start < timeout: + all_done = True + for job_id in job_ids: + response = self._client.get( + f"/api/w/{self._workspace}/jobs_u/get/{job_id}", + ) + if response.status_code // 100 != 2: + all_done = False + break + job = response.json() + if job.get("type") != "CompletedJob": + all_done = False + break + if all_done: + return + time.sleep(2) + raise TimeoutError(f"Timed out waiting for jobs {job_ids} after {timeout}s") + + def delete_workspace(self, workspace_id: str): + """Delete a workspace.""" + print(f"Deleting workspace {workspace_id}") + response = self._client.post( + f"/api/w/{workspace_id}/workspaces/delete", + ) + if response.status_code // 100 != 2 and response.status_code != 404: + print(f"Warning: failed to delete workspace {workspace_id}: {response.content.decode()}") + def create_agent_token(self, worker_group="agent", tags=None, exp=None): """ Create an agent JWT token using superadmin privilege. @@ -418,3 +584,163 @@ class WindmillClient: token = response.content.decode().strip('"') print(f"Created agent token: {token}") return token + + +GITEA_HOST_URL = os.environ.get("GITEA_HOST_URL", "http://localhost:3000") +GITEA_DOCKER_URL = os.environ.get("GITEA_DOCKER_URL", "http://gitea:3000") +GITEA_ADMIN_USER = "windmill" +GITEA_ADMIN_PASSWORD = "password123!" +GITEA_ADMIN_EMAIL = "windmill@windmill.dev" + + +class GiteaClient: + _host_url: str + _docker_url: str + _token: str + + def __init__(self): + self._host_url = GITEA_HOST_URL + self._docker_url = GITEA_DOCKER_URL + self._token = None + + def setup_admin(self): + """Create the admin user in Gitea (idempotent) and get an API token.""" + with httpx.Client(base_url=self._host_url, timeout=30.0) as client: + # Create admin user (ignore 422 if exists) + resp = client.post( + "/api/v1/admin/users", + json={ + "username": GITEA_ADMIN_USER, + "password": GITEA_ADMIN_PASSWORD, + "email": GITEA_ADMIN_EMAIL, + "must_change_password": False, + "visibility": "public", + }, + headers={"Content-Type": "application/json"}, + auth=(GITEA_ADMIN_USER, GITEA_ADMIN_PASSWORD), + ) + if resp.status_code == 201: + print(f"Created Gitea admin user '{GITEA_ADMIN_USER}'") + elif resp.status_code == 422: + print(f"Gitea admin user '{GITEA_ADMIN_USER}' already exists") + elif resp.status_code == 401: + # Admin user doesn't exist yet; use the Gitea setup API + resp2 = client.post( + "/user/sign_up", + data={ + "user_name": GITEA_ADMIN_USER, + "password": GITEA_ADMIN_PASSWORD, + "retype": GITEA_ADMIN_PASSWORD, + "email": GITEA_ADMIN_EMAIL, + }, + ) + if resp2.status_code // 100 != 2 and resp2.status_code != 303: + # Try the API endpoint for creating the first user + resp3 = client.post( + "/api/v1/admin/users", + json={ + "username": GITEA_ADMIN_USER, + "password": GITEA_ADMIN_PASSWORD, + "email": GITEA_ADMIN_EMAIL, + "must_change_password": False, + }, + ) + if resp3.status_code // 100 != 2: + raise Exception(f"Failed to create Gitea user: {resp3.status_code} {resp3.text}") + print(f"Created Gitea admin user '{GITEA_ADMIN_USER}' via signup") + else: + raise Exception(f"Failed to create Gitea user: {resp.status_code} {resp.text}") + + # Create API token + token_name = f"integration-test-{uuid.uuid4().hex[:8]}" + resp = client.post( + f"/api/v1/users/{GITEA_ADMIN_USER}/tokens", + json={"name": token_name, "scopes": ["all"]}, + auth=(GITEA_ADMIN_USER, GITEA_ADMIN_PASSWORD), + ) + if resp.status_code // 100 != 2: + raise Exception(f"Failed to create Gitea token: {resp.status_code} {resp.text}") + self._token = resp.json()["sha1"] + print(f"Created Gitea API token: {token_name}") + + def _headers(self): + return { + "Authorization": f"token {self._token}", + "Content-Type": "application/json", + } + + def create_repo(self, name: str) -> str: + """Create a repo and return the docker-internal clone URL with credentials.""" + with httpx.Client(base_url=self._host_url, timeout=30.0) as client: + resp = client.post( + "/api/v1/user/repos", + json={ + "name": name, + "auto_init": True, + "default_branch": "main", + "private": False, + }, + headers=self._headers(), + ) + if resp.status_code // 100 != 2: + raise Exception(f"Failed to create repo {name}: {resp.status_code} {resp.text}") + print(f"Created Gitea repo: {name}") + return f"{self._docker_url}/{GITEA_ADMIN_USER}/{name}.git" + + def get_host_clone_url(self, name: str) -> str: + """Return host-accessible clone URL with credentials.""" + from urllib.parse import urlparse + parsed = urlparse(self._host_url) + return f"http://{GITEA_ADMIN_USER}:{GITEA_ADMIN_PASSWORD}@{parsed.netloc}/{GITEA_ADMIN_USER}/{name}.git" + + def get_docker_clone_url(self, name: str) -> str: + """Return clone URL accessible from the Windmill backend (docker or local).""" + from urllib.parse import urlparse + parsed = urlparse(self._docker_url) + return f"http://{GITEA_ADMIN_USER}:{GITEA_ADMIN_PASSWORD}@{parsed.netloc}/{GITEA_ADMIN_USER}/{name}.git" + + def create_file(self, repo_name: str, file_path: str, content: str, branch: str = "main"): + """Create or update a file in the repo via Gitea API.""" + import base64 + encoded = base64.b64encode(content.encode()).decode() + with httpx.Client(base_url=self._host_url, timeout=30.0) as client: + # Check if file exists (to get SHA for update) + resp = client.get( + f"/api/v1/repos/{GITEA_ADMIN_USER}/{repo_name}/contents/{file_path}", + params={"ref": branch}, + headers=self._headers(), + ) + body = { + "content": encoded, + "message": f"Add {file_path}", + "branch": branch, + } + if resp.status_code == 200: + # File exists — update with PUT + body["sha"] = resp.json()["sha"] + resp = client.put( + f"/api/v1/repos/{GITEA_ADMIN_USER}/{repo_name}/contents/{file_path}", + json=body, + headers=self._headers(), + ) + else: + # File doesn't exist — create with POST + resp = client.post( + f"/api/v1/repos/{GITEA_ADMIN_USER}/{repo_name}/contents/{file_path}", + json=body, + headers=self._headers(), + ) + if resp.status_code // 100 != 2: + raise Exception(f"Failed to create file {file_path}: {resp.status_code} {resp.text}") + print(f"Created file {file_path} in {repo_name}") + + def delete_repo(self, name: str): + with httpx.Client(base_url=self._host_url, timeout=30.0) as client: + resp = client.delete( + f"/api/v1/repos/{GITEA_ADMIN_USER}/{name}", + headers=self._headers(), + ) + if resp.status_code // 100 == 2 or resp.status_code == 404: + print(f"Deleted Gitea repo: {name}") + else: + print(f"Warning: failed to delete repo {name}: {resp.status_code}") From ec20d76216492086842c4f5e4e3b36727a5631e9 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Thu, 12 Mar 2026 15:00:30 +0100 Subject: [PATCH 05/25] feat: add auto_commit option to Kafka triggers with advanced UI badges (#8317) * feat: add auto_commit option to Kafka triggers with manual commit API Add ability to disable auto-commit on Kafka triggers so users can manually commit offsets after processing messages. This prevents message loss when processing fails. Changes: - Add `auto_commit` column to kafka_trigger table (default true) - Add POST /kafka_triggers/commit_offsets/{path} endpoint using BaseConsumer with manual assign() to avoid rebalance - Enrich trigger_info payload with partition and offset fields - Conditionally commit based on auto_commit setting - Add auto-commit toggle to frontend Kafka trigger config - Add commitKafkaOffsets helpers to Python and TypeScript SDKs - Add integration tests for auto_commit DB defaults Co-Authored-By: Claude Opus 4.6 * feat: use DB-based pending commits for kafka manual offset commit Co-Authored-By: Claude Opus 4.5 * feat: pass trigger_path to all v2 preprocessors, secure commit_offsets endpoint, fix commit semantics - Add trigger_path to v2 preprocessor event for all trigger types (kafka, nats, sqs, mqtt, gcp, postgres, websocket, http, email) - Secure commit_offsets endpoint: infer trigger from job token (OptJobAuthed) instead of requiring trigger path parameter - Fix auto_commit: only commit offset after successful job push - Fix pending commits: commit offset+1 (Kafka semantics) and use CommitMode::Sync - Update TS/Python clients and frontend preprocessor templates Co-Authored-By: Claude Opus 4.6 * feat: add advanced section badges and reorganize kafka trigger settings Co-Authored-By: Claude Opus 4.6 * fix: remove dead wm_trigger assertions from kafka e2e test Co-Authored-By: Claude Opus 4.6 * sqlx * refactor: remove unused advancedCollapsed state from all trigger editors Co-Authored-By: Claude Opus 4.6 * update ref * chore: update ee-repo-ref to ed2c9d360e6fab866b9744cc79f50038d1fc7152 This commit updates the EE repository reference after PR #452 was merged in windmill-ee-private. Previous ee-repo-ref: 5b31116a1d5a042c6a780732901cfd89584d1773 New ee-repo-ref: ed2c9d360e6fab866b9744cc79f50038d1fc7152 Automated by sync-ee-ref workflow. * fix: use path-based auth for kafka commit_offsets endpoint Co-Authored-By: Claude Opus 4.6 * chore: update ee-repo-ref to fcd3ea52b0cc94fbe1159baf662a38da947456de This commit updates the EE repository reference after PR #457 was merged in windmill-ee-private. Previous ee-repo-ref: b3a5c33c92cb1b2caf7a65986d71da291ff72a35 New ee-repo-ref: fcd3ea52b0cc94fbe1159baf662a38da947456de Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: windmill-internal-app[bot] Co-authored-by: Ruben Fiszel --- ...7fa3ab402e33edfca46daa95b52e525424586.json | 41 +++++++ ...cb24cc635836276ee6faa2438f494bfbce04.json} | 5 +- ...68554a6a4291426c09dd5b04fd832f9d31208.json | 23 ++++ ...e81abb3ec627578f2d14f0ce33896dc6ab4cf.json | 23 ++++ ...623b991c938e40d0db25290b03dc0577c2740.json | 23 ++++ ...c7e60d23e9827f23c0de5b546ed56697d54a.json} | 22 +++- ...55c13a18dfd26e237a8218d3006e2898b5aa3.json | 23 ++++ ...920837354d91a0ae16b1236c1267da71765f.json} | 5 +- ...15565490a271050dcc4b5aac2b730ae3a55b9.json | 14 +++ ...927c07832badb2c9145d5996b829163bdf7d9.json | 23 ++++ ...c047cf3b7ce22ce868db1b1a63e6c10d0d12b.json | 15 +++ ...fcbb5b647737e0366529a2f418218b1a74679.json | 18 +++ backend/Cargo.lock | 6 +- backend/ee-repo-ref.txt | 2 +- .../20260312000000_kafka_auto_commit.down.sql | 2 + .../20260312000000_kafka_auto_commit.up.sql | 14 +++ backend/summarized_schema.txt | 4 +- .../tests/trigger_e2e.rs | 18 +-- .../tests/triggers.rs | 70 ++++++++--- backend/windmill-api/openapi.yaml | 45 +++++++ backend/windmill-api/src/capture.rs | 1 + .../windmill-api/src/triggers/http/handler.rs | 1 + .../src/triggers/http/http_trigger_args.rs | 13 +- .../src/listener.rs | 3 +- backend/windmill-trigger/src/listener.rs | 12 +- .../triggers/TriggerAdvancedBadges.svelte | 28 +++++ .../email/EmailTriggerEditorInner.svelte | 7 +- .../triggers/gcp/GcpTriggerEditorInner.svelte | 9 +- .../triggers/http/RouteEditorInner.svelte | 9 ++ .../kafka/KafkaTriggerEditorInner.svelte | 112 +++++++++++++----- .../kafka/KafkaTriggersConfigSection.svelte | 23 ---- .../lib/components/triggers/kafka/utils.ts | 1 + .../mqtt/MqttTriggerEditorInner.svelte | 10 +- .../nats/NatsTriggerEditorInner.svelte | 7 +- .../PostgresTriggerEditorInner.svelte | 7 +- .../schedules/ScheduleEditorInner.svelte | 10 ++ .../triggers/sqs/SqsTriggerEditorInner.svelte | 7 +- .../WebsocketTriggerEditorInner.svelte | 12 +- frontend/src/lib/script_helpers.ts | 64 ++++++---- python-client/wmill/wmill/client.py | 25 ++++ typescript-client/build.sh | 4 +- typescript-client/client.d.ts | 13 ++ typescript-client/client.ts | 22 ++++ 43 files changed, 677 insertions(+), 119 deletions(-) create mode 100644 backend/.sqlx/query-038d2fde90fa9e99e30d15161777fa3ab402e33edfca46daa95b52e525424586.json rename backend/.sqlx/{query-12631fecee6aa11a45cf5c8d101c0dd8de50ac9b57e68198f637038d344fdd46.json => query-072e5ab78f929c6b7264f98c1588cb24cc635836276ee6faa2438f494bfbce04.json} (54%) create mode 100644 backend/.sqlx/query-1df610a583e86edb70c374fd66c68554a6a4291426c09dd5b04fd832f9d31208.json create mode 100644 backend/.sqlx/query-45fc21026fa76e5d69f00a68a7be81abb3ec627578f2d14f0ce33896dc6ab4cf.json create mode 100644 backend/.sqlx/query-4b2a29b3ef7ec4802d81ec4b706623b991c938e40d0db25290b03dc0577c2740.json rename backend/.sqlx/{query-7e3bfb33fb771aec39b43a7550091ce7c9b1261b52d10f4a7f3273fed3c916df.json => query-4cf4be7a981173d3f242887d9313c7e60d23e9827f23c0de5b546ed56697d54a.json} (61%) create mode 100644 backend/.sqlx/query-50807b807bb901a380926798be655c13a18dfd26e237a8218d3006e2898b5aa3.json rename backend/.sqlx/{query-4b5a711986017654bdd495893a16ddc6ab09c98cd8723865cbd341404bc6a02f.json => query-5dd6315ec270c268e905262e4b0a920837354d91a0ae16b1236c1267da71765f.json} (63%) create mode 100644 backend/.sqlx/query-80bad96cbec6b5eca57a6380e7515565490a271050dcc4b5aac2b730ae3a55b9.json create mode 100644 backend/.sqlx/query-c2f38c9e09aac73d10e8f327715927c07832badb2c9145d5996b829163bdf7d9.json create mode 100644 backend/.sqlx/query-ef15599f532fab2cbb487542ffec047cf3b7ce22ce868db1b1a63e6c10d0d12b.json create mode 100644 backend/.sqlx/query-f67e5c96eb9cb35953d4c3e83e0fcbb5b647737e0366529a2f418218b1a74679.json create mode 100644 backend/migrations/20260312000000_kafka_auto_commit.down.sql create mode 100644 backend/migrations/20260312000000_kafka_auto_commit.up.sql create mode 100644 frontend/src/lib/components/triggers/TriggerAdvancedBadges.svelte diff --git a/backend/.sqlx/query-038d2fde90fa9e99e30d15161777fa3ab402e33edfca46daa95b52e525424586.json b/backend/.sqlx/query-038d2fde90fa9e99e30d15161777fa3ab402e33edfca46daa95b52e525424586.json new file mode 100644 index 0000000000..5dcf8e0792 --- /dev/null +++ b/backend/.sqlx/query-038d2fde90fa9e99e30d15161777fa3ab402e33edfca46daa95b52e525424586.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, topic, partition, \"offset\" FROM kafka_pending_commits\n WHERE workspace_id = $1 AND kafka_trigger_path = $2\n ORDER BY id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "topic", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "partition", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "offset", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "038d2fde90fa9e99e30d15161777fa3ab402e33edfca46daa95b52e525424586" +} diff --git a/backend/.sqlx/query-12631fecee6aa11a45cf5c8d101c0dd8de50ac9b57e68198f637038d344fdd46.json b/backend/.sqlx/query-072e5ab78f929c6b7264f98c1588cb24cc635836276ee6faa2438f494bfbce04.json similarity index 54% rename from backend/.sqlx/query-12631fecee6aa11a45cf5c8d101c0dd8de50ac9b57e68198f637038d344fdd46.json rename to backend/.sqlx/query-072e5ab78f929c6b7264f98c1588cb24cc635836276ee6faa2438f494bfbce04.json index c452c33018..812c323e74 100644 --- a/backend/.sqlx/query-12631fecee6aa11a45cf5c8d101c0dd8de50ac9b57e68198f637038d344fdd46.json +++ b/backend/.sqlx/query-072e5ab78f929c6b7264f98c1588cb24cc635836276ee6faa2438f494bfbce04.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n auto_offset_reset = $5,\n script_path = $6,\n path = $7,\n is_flow = $8,\n edited_by = $9,\n email = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ", + "query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n auto_offset_reset = $5,\n auto_commit = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n edited_by = $10,\n email = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ", "describe": { "columns": [], "parameters": { @@ -10,6 +10,7 @@ "VarcharArray", "JsonbArray", "Varchar", + "Bool", "Varchar", "Varchar", "Bool", @@ -24,5 +25,5 @@ }, "nullable": [] }, - "hash": "12631fecee6aa11a45cf5c8d101c0dd8de50ac9b57e68198f637038d344fdd46" + "hash": "072e5ab78f929c6b7264f98c1588cb24cc635836276ee6faa2438f494bfbce04" } diff --git a/backend/.sqlx/query-1df610a583e86edb70c374fd66c68554a6a4291426c09dd5b04fd832f9d31208.json b/backend/.sqlx/query-1df610a583e86edb70c374fd66c68554a6a4291426c09dd5b04fd832f9d31208.json new file mode 100644 index 0000000000..babf3ffbcc --- /dev/null +++ b/backend/.sqlx/query-1df610a583e86edb70c374fd66c68554a6a4291426c09dd5b04fd832f9d31208.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT reset_offset FROM kafka_trigger WHERE workspace_id = $1 AND path = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "reset_offset", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "1df610a583e86edb70c374fd66c68554a6a4291426c09dd5b04fd832f9d31208" +} diff --git a/backend/.sqlx/query-45fc21026fa76e5d69f00a68a7be81abb3ec627578f2d14f0ce33896dc6ab4cf.json b/backend/.sqlx/query-45fc21026fa76e5d69f00a68a7be81abb3ec627578f2d14f0ce33896dc6ab4cf.json new file mode 100644 index 0000000000..b5873760fc --- /dev/null +++ b/backend/.sqlx/query-45fc21026fa76e5d69f00a68a7be81abb3ec627578f2d14f0ce33896dc6ab4cf.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path,\n is_flow, workspace_id, edited_by, email, auto_commit\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "VarcharArray", + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Varchar", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "45fc21026fa76e5d69f00a68a7be81abb3ec627578f2d14f0ce33896dc6ab4cf" +} diff --git a/backend/.sqlx/query-4b2a29b3ef7ec4802d81ec4b706623b991c938e40d0db25290b03dc0577c2740.json b/backend/.sqlx/query-4b2a29b3ef7ec4802d81ec4b706623b991c938e40d0db25290b03dc0577c2740.json new file mode 100644 index 0000000000..6dd5c799e7 --- /dev/null +++ b/backend/.sqlx/query-4b2a29b3ef7ec4802d81ec4b706623b991c938e40d0db25290b03dc0577c2740.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT auto_commit FROM kafka_trigger WHERE workspace_id = $1 AND path = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "auto_commit", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "4b2a29b3ef7ec4802d81ec4b706623b991c938e40d0db25290b03dc0577c2740" +} diff --git a/backend/.sqlx/query-7e3bfb33fb771aec39b43a7550091ce7c9b1261b52d10f4a7f3273fed3c916df.json b/backend/.sqlx/query-4cf4be7a981173d3f242887d9313c7e60d23e9827f23c0de5b546ed56697d54a.json similarity index 61% rename from backend/.sqlx/query-7e3bfb33fb771aec39b43a7550091ce7c9b1261b52d10f4a7f3273fed3c916df.json rename to backend/.sqlx/query-4cf4be7a981173d3f242887d9313c7e60d23e9827f23c0de5b546ed56697d54a.json index 23652a2571..9b76b7f048 100644 --- a/backend/.sqlx/query-7e3bfb33fb771aec39b43a7550091ce7c9b1261b52d10f4a7f3273fed3c916df.json +++ b/backend/.sqlx/query-4cf4be7a981173d3f242887d9313c7e60d23e9827f23c0de5b546ed56697d54a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT kafka_resource_path, topics, group_id, mode AS \"mode: String\"\n FROM kafka_trigger\n WHERE workspace_id = $1 AND path = $2\n ", + "query": "\n SELECT kafka_resource_path, topics, group_id, mode AS \"mode: String\",\n auto_offset_reset, auto_commit, reset_offset\n FROM kafka_trigger\n WHERE workspace_id = $1 AND path = $2\n ", "describe": { "columns": [ { @@ -33,6 +33,21 @@ } } } + }, + { + "ordinal": 4, + "name": "auto_offset_reset", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "auto_commit", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "reset_offset", + "type_info": "Bool" } ], "parameters": { @@ -42,11 +57,14 @@ ] }, "nullable": [ + false, + false, + false, false, false, false, false ] }, - "hash": "7e3bfb33fb771aec39b43a7550091ce7c9b1261b52d10f4a7f3273fed3c916df" + "hash": "4cf4be7a981173d3f242887d9313c7e60d23e9827f23c0de5b546ed56697d54a" } diff --git a/backend/.sqlx/query-50807b807bb901a380926798be655c13a18dfd26e237a8218d3006e2898b5aa3.json b/backend/.sqlx/query-50807b807bb901a380926798be655c13a18dfd26e237a8218d3006e2898b5aa3.json new file mode 100644 index 0000000000..ec667519e4 --- /dev/null +++ b/backend/.sqlx/query-50807b807bb901a380926798be655c13a18dfd26e237a8218d3006e2898b5aa3.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT auto_commit\n FROM kafka_trigger\n WHERE workspace_id = $1 AND path = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "auto_commit", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "50807b807bb901a380926798be655c13a18dfd26e237a8218d3006e2898b5aa3" +} diff --git a/backend/.sqlx/query-4b5a711986017654bdd495893a16ddc6ab09c98cd8723865cbd341404bc6a02f.json b/backend/.sqlx/query-5dd6315ec270c268e905262e4b0a920837354d91a0ae16b1236c1267da71765f.json similarity index 63% rename from backend/.sqlx/query-4b5a711986017654bdd495893a16ddc6ab09c98cd8723865cbd341404bc6a02f.json rename to backend/.sqlx/query-5dd6315ec270c268e905262e4b0a920837354d91a0ae16b1236c1267da71765f.json index d90a467380..936a4650f6 100644 --- a/backend/.sqlx/query-4b5a711986017654bdd495893a16ddc6ab09c98cd8723865cbd341404bc6a02f.json +++ b/backend/.sqlx/query-5dd6315ec270c268e905262e4b0a920837354d91a0ae16b1236c1267da71765f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n auto_offset_reset,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15\n )\n ", + "query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n auto_offset_reset,\n auto_commit,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ", "describe": { "columns": [], "parameters": { @@ -12,6 +12,7 @@ "VarcharArray", "JsonbArray", "Varchar", + "Bool", "Varchar", "Bool", { @@ -35,5 +36,5 @@ }, "nullable": [] }, - "hash": "4b5a711986017654bdd495893a16ddc6ab09c98cd8723865cbd341404bc6a02f" + "hash": "5dd6315ec270c268e905262e4b0a920837354d91a0ae16b1236c1267da71765f" } diff --git a/backend/.sqlx/query-80bad96cbec6b5eca57a6380e7515565490a271050dcc4b5aac2b730ae3a55b9.json b/backend/.sqlx/query-80bad96cbec6b5eca57a6380e7515565490a271050dcc4b5aac2b730ae3a55b9.json new file mode 100644 index 0000000000..31f2767fe8 --- /dev/null +++ b/backend/.sqlx/query-80bad96cbec6b5eca57a6380e7515565490a271050dcc4b5aac2b730ae3a55b9.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM kafka_pending_commits WHERE id = ANY($1)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [] + }, + "hash": "80bad96cbec6b5eca57a6380e7515565490a271050dcc4b5aac2b730ae3a55b9" +} diff --git a/backend/.sqlx/query-c2f38c9e09aac73d10e8f327715927c07832badb2c9145d5996b829163bdf7d9.json b/backend/.sqlx/query-c2f38c9e09aac73d10e8f327715927c07832badb2c9145d5996b829163bdf7d9.json new file mode 100644 index 0000000000..7415b311f1 --- /dev/null +++ b/backend/.sqlx/query-c2f38c9e09aac73d10e8f327715927c07832badb2c9145d5996b829163bdf7d9.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE kafka_trigger SET reset_offset = true, server_id = NULL WHERE workspace_id = $1 AND path = $2 RETURNING true", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "c2f38c9e09aac73d10e8f327715927c07832badb2c9145d5996b829163bdf7d9" +} diff --git a/backend/.sqlx/query-ef15599f532fab2cbb487542ffec047cf3b7ce22ce868db1b1a63e6c10d0d12b.json b/backend/.sqlx/query-ef15599f532fab2cbb487542ffec047cf3b7ce22ce868db1b1a63e6c10d0d12b.json new file mode 100644 index 0000000000..238977d563 --- /dev/null +++ b/backend/.sqlx/query-ef15599f532fab2cbb487542ffec047cf3b7ce22ce868db1b1a63e6c10d0d12b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE kafka_trigger SET reset_offset = false WHERE workspace_id = $1 AND path = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "ef15599f532fab2cbb487542ffec047cf3b7ce22ce868db1b1a63e6c10d0d12b" +} diff --git a/backend/.sqlx/query-f67e5c96eb9cb35953d4c3e83e0fcbb5b647737e0366529a2f418218b1a74679.json b/backend/.sqlx/query-f67e5c96eb9cb35953d4c3e83e0fcbb5b647737e0366529a2f418218b1a74679.json new file mode 100644 index 0000000000..5797d761e9 --- /dev/null +++ b/backend/.sqlx/query-f67e5c96eb9cb35953d4c3e83e0fcbb5b647737e0366529a2f418218b1a74679.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO kafka_pending_commits (workspace_id, kafka_trigger_path, topic, partition, \"offset\")\n VALUES ($1, $2, $3, $4, $5)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Int4", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "f67e5c96eb9cb35953d4c3e83e0fcbb5b647737e0366529a2f418218b1a74679" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 23e6f2a4b1..b10179c658 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -16870,7 +16870,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.653.0" +version = "1.654.0" dependencies = [ "anyhow", "rustpython-ast", @@ -16950,7 +16950,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.653.0" +version = "1.654.0" dependencies = [ "anyhow", "serde", @@ -16980,7 +16980,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.653.0" +version = "1.654.0" dependencies = [ "anyhow", "serde-wasm-bindgen", diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 18e2279e37..a048a9b463 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -85bcbfc8e952842c555156d1050b2a024e7e37a3 \ No newline at end of file +fcd3ea52b0cc94fbe1159baf662a38da947456de \ No newline at end of file diff --git a/backend/migrations/20260312000000_kafka_auto_commit.down.sql b/backend/migrations/20260312000000_kafka_auto_commit.down.sql new file mode 100644 index 0000000000..58d1ad2659 --- /dev/null +++ b/backend/migrations/20260312000000_kafka_auto_commit.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS kafka_pending_commits; +ALTER TABLE kafka_trigger DROP COLUMN auto_commit; diff --git a/backend/migrations/20260312000000_kafka_auto_commit.up.sql b/backend/migrations/20260312000000_kafka_auto_commit.up.sql new file mode 100644 index 0000000000..643e2ce1f3 --- /dev/null +++ b/backend/migrations/20260312000000_kafka_auto_commit.up.sql @@ -0,0 +1,14 @@ +ALTER TABLE kafka_trigger ADD COLUMN auto_commit BOOLEAN NOT NULL DEFAULT TRUE; + +CREATE TABLE kafka_pending_commits ( + id BIGSERIAL PRIMARY KEY, + workspace_id VARCHAR(50) NOT NULL, + kafka_trigger_path VARCHAR(255) NOT NULL, + topic VARCHAR(255) NOT NULL, + partition INTEGER NOT NULL, + "offset" BIGINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + FOREIGN KEY (workspace_id, kafka_trigger_path) REFERENCES kafka_trigger(workspace_id, path) ON DELETE CASCADE +); + +CREATE INDEX idx_kafka_pending_commits_trigger ON kafka_pending_commits (workspace_id, kafka_trigger_path); diff --git a/backend/summarized_schema.txt b/backend/summarized_schema.txt index 3ece1ce7ef..2f86dcb322 100644 --- a/backend/summarized_schema.txt +++ b/backend/summarized_schema.txt @@ -109,7 +109,9 @@ job_result_stream_v2: job_id(uuid), workspace_id(text), stream(text), idx(int) job_settings: job_id(uuid), runnable_settings(bigint) job_stats: workspace_id(char), job_id(uuid), metric_id(char), metric_name(char), metric_kind(metric_kind), scalar_int(int), scalar_float(float), timestamps(ts), timeseries_int(int[]), timeseries_float(float[]) FK: (workspace_id) -> workspace(id) -kafka_trigger: path(char), kafka_resource_path(char), topics(char), group_id(char), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), server_id(char), last_server_ping(ts), error(text), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode), filters(jsonb[]) +kafka_pending_commits: id(bigint), workspace_id(char), kafka_trigger_path(char), topic(char), partition(int), offset(bigint), created_at(ts) + FK: (workspace_id, kafka_trigger_path) -> kafka_trigger(workspace_id, path) +kafka_trigger: path(char), kafka_resource_path(char), topics(char), group_id(char), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), server_id(char), last_server_ping(ts), error(text), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode), filters(jsonb[]), auto_commit(bool) log_file: hostname(char), log_ts(ts), ok_lines(bigint), err_lines(bigint), mode(log_mode), worker_group(char), file_path(char), json_fmt(bool) magic_link: email(char), token(char), expiration(ts) mcp_oauth_client: mcp_server_url(text), client_id(text), client_secret(text), client_secret_expires_at(ts), token_endpoint(text), created_at(ts) diff --git a/backend/windmill-api-integration-tests/tests/trigger_e2e.rs b/backend/windmill-api-integration-tests/tests/trigger_e2e.rs index 3b0e32749a..1d4f122bcc 100644 --- a/backend/windmill-api-integration-tests/tests/trigger_e2e.rs +++ b/backend/windmill-api-integration-tests/tests/trigger_e2e.rs @@ -251,7 +251,8 @@ async fn test_websocket_e2e(db: Pool) -> anyhow::Result<()> { "test-workspace", "test-user", "test@windmill.dev", - &[json!({"type": "RawMessage", "content": "hello from e2e test"})] as &[serde_json::Value], + &[json!({"type": "RawMessage", "content": "hello from e2e test"})] + as &[serde_json::Value], ) .execute(&db) .await?; @@ -302,9 +303,11 @@ async fn test_postgres_e2e(db: Pool) -> anyhow::Result<()> { sqlx::query("CREATE TABLE test_trigger_table (id serial PRIMARY KEY, data text)") .execute(&db) .await?; - sqlx::query(&format!("CREATE PUBLICATION {pub_name} FOR TABLE test_trigger_table")) - .execute(&db) - .await?; + sqlx::query(&format!( + "CREATE PUBLICATION {pub_name} FOR TABLE test_trigger_table" + )) + .execute(&db) + .await?; sqlx::query(&format!( "SELECT pg_create_logical_replication_slot('{slot_name}', 'pgoutput')" )) @@ -313,10 +316,9 @@ async fn test_postgres_e2e(db: Pool) -> anyhow::Result<()> { // Extract the test DB name from the pool so the resource points here, // not at the main windmill database. - let test_db_name: String = - sqlx::query_scalar("SELECT current_database()") - .fetch_one(&db) - .await?; + let test_db_name: String = sqlx::query_scalar("SELECT current_database()") + .fetch_one(&db) + .await?; insert_resource( &db, diff --git a/backend/windmill-api-integration-tests/tests/triggers.rs b/backend/windmill-api-integration-tests/tests/triggers.rs index 46854d566e..b55f24397c 100644 --- a/backend/windmill-api-integration-tests/tests/triggers.rs +++ b/backend/windmill-api-integration-tests/tests/triggers.rs @@ -214,12 +214,9 @@ async fn test_capture_delete(db: Pool) -> anyhow::Result<()> { .execute(&db) .await?; - let count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM capture WHERE id = $1", - id, - ) - .fetch_one(&db) - .await?; + let count = sqlx::query_scalar!("SELECT COUNT(*) FROM capture WHERE id = $1", id,) + .fetch_one(&db) + .await?; assert_eq!(count, Some(0)); @@ -385,7 +382,10 @@ async fn test_capture_api_list_captures(db: Pool) -> anyhow::Result<() .send() .await?; - assert!(response.status().is_success(), "list captures should succeed"); + assert!( + response.status().is_success(), + "list captures should succeed" + ); let captures: Vec = response.json().await?; assert_eq!(captures.len(), 3); @@ -480,12 +480,9 @@ async fn test_capture_api_delete(db: Pool) -> anyhow::Result<()> { assert!(response.status().is_success(), "delete should succeed"); - let count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM capture WHERE id = $1", - id, - ) - .fetch_one(&db) - .await?; + let count = sqlx::query_scalar!("SELECT COUNT(*) FROM capture WHERE id = $1", id,) + .fetch_one(&db) + .await?; assert_eq!(count, Some(0)); @@ -933,7 +930,8 @@ async fn test_kafka_trigger_insert(db: Pool) -> anyhow::Result<()> { let trigger = sqlx::query!( r#" - SELECT kafka_resource_path, topics, group_id, mode AS "mode: String" + SELECT kafka_resource_path, topics, group_id, mode AS "mode: String", + auto_offset_reset, auto_commit, reset_offset FROM kafka_trigger WHERE workspace_id = $1 AND path = $2 "#, @@ -947,6 +945,50 @@ async fn test_kafka_trigger_insert(db: Pool) -> anyhow::Result<()> { assert_eq!(trigger.topics, vec!["topic-a", "topic-b"]); assert_eq!(trigger.group_id, "my-consumer-group"); assert_eq!(trigger.mode, "enabled"); + assert_eq!(trigger.auto_offset_reset, "latest"); + assert_eq!(trigger.auto_commit, true); + assert_eq!(trigger.reset_offset, false); + + Ok(()) +} + +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_kafka_trigger_insert_auto_commit_disabled(db: Pool) -> anyhow::Result<()> { + sqlx::query!( + r#" + INSERT INTO kafka_trigger ( + path, kafka_resource_path, topics, group_id, script_path, + is_flow, workspace_id, edited_by, email, auto_commit + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + "#, + "f/test/kafka_trigger_no_commit", + "u/admin/kafka_resource", + &["topic-c"] as &[&str], + "my-consumer-group-2", + "f/test/kafka_handler", + false, + "test-workspace", + "test-user", + "test@windmill.dev", + false, + ) + .execute(&db) + .await?; + + let trigger = sqlx::query!( + r#" + SELECT auto_commit + FROM kafka_trigger + WHERE workspace_id = $1 AND path = $2 + "#, + "test-workspace", + "f/test/kafka_trigger_no_commit", + ) + .fetch_one(&db) + .await?; + + assert_eq!(trigger.auto_commit, false); Ok(()) } diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 9bb25f5589..9ec79f84d4 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -12017,6 +12017,39 @@ paths: "200": description: kafka trigger offsets reset successfully + /w/{workspace}/kafka_triggers/commit_offsets/{path}: + post: + summary: commit kafka offsets for a specific trigger + operationId: commitKafkaOffsets + tags: + - kafka_trigger + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + requestBody: + description: offsets to commit + required: true + content: + application/json: + schema: + type: object + properties: + topic: + type: string + partition: + type: integer + format: int32 + offset: + type: integer + format: int64 + required: + - topic + - partition + - offset + responses: + "200": + description: kafka offsets committed successfully + /w/{workspace}/nats_triggers/create: post: summary: create nats trigger @@ -22098,6 +22131,10 @@ components: - earliest default: latest description: "Initial offset behavior when consumer group has no committed offset. 'latest' starts from new messages only, 'earliest' starts from the beginning." + auto_commit: + type: boolean + default: true + description: "When true (default), offsets are committed automatically after receiving each message. When false, you must manually commit offsets using the commit_offsets endpoint." server_id: type: string description: ID of the server currently handling this trigger (internal) @@ -22165,6 +22202,10 @@ components: - earliest default: latest description: "Initial offset behavior when consumer group has no committed offset." + auto_commit: + type: boolean + default: true + description: "When true (default), offsets are committed automatically after receiving each message. When false, you must manually commit offsets using the commit_offsets endpoint." mode: $ref: "#/components/schemas/TriggerMode" error_handler_path: @@ -22224,6 +22265,10 @@ components: - earliest default: latest description: "Initial offset behavior when consumer group has no committed offset." + auto_commit: + type: boolean + default: true + description: "When true (default), offsets are committed automatically after receiving each message. When false, you must manually commit offsets using the commit_offsets endpoint." path: type: string description: The unique path identifier for this trigger diff --git a/backend/windmill-api/src/capture.rs b/backend/windmill-api/src/capture.rs index bcc287ec04..277504b481 100644 --- a/backend/windmill-api/src/capture.rs +++ b/backend/windmill-api/src/capture.rs @@ -1012,6 +1012,7 @@ async fn http_payload( .to_v2_preprocessor_args( &http_trigger_config.route_path, &route_path, + "", ¶ms, headers, query, diff --git a/backend/windmill-api/src/triggers/http/handler.rs b/backend/windmill-api/src/triggers/http/handler.rs index 9d95ead840..fcb883f089 100644 --- a/backend/windmill-api/src/triggers/http/handler.rs +++ b/backend/windmill-api/src/triggers/http/handler.rs @@ -468,6 +468,7 @@ async fn route_job( .to_args_from_format( &trigger.route_path, &called_path, + &trigger.path, ¶ms, runnable_format, trigger.wrap_body, diff --git a/backend/windmill-api/src/triggers/http/http_trigger_args.rs b/backend/windmill-api/src/triggers/http/http_trigger_args.rs index 4866f9be34..7df1e9f200 100644 --- a/backend/windmill-api/src/triggers/http/http_trigger_args.rs +++ b/backend/windmill-api/src/triggers/http/http_trigger_args.rs @@ -68,6 +68,7 @@ struct HttpTriggerPreprocessorEvent<'a> { kind: String, route: &'a str, path: &'a str, + trigger_path: &'a str, body: Box, raw_string: Option, params: &'a HashMap, @@ -117,6 +118,7 @@ impl HttpTriggerArgs { self, route_path: &str, called_path: &str, + trigger_path: &str, params: &HashMap, format: RunnableFormat, wrap_body: bool, @@ -126,7 +128,14 @@ impl HttpTriggerArgs { match format { RunnableFormat { has_preprocessor: true, version: RunnableFormatVersion::V2 } => { // we don't care about wrap_body in v2 - self.to_v2_preprocessor_args(route_path, called_path, params, headers, query) + self.to_v2_preprocessor_args( + route_path, + called_path, + trigger_path, + params, + headers, + query, + ) } RunnableFormat { has_preprocessor: true, version: RunnableFormatVersion::V1 } => self .to_v1_preprocessor_args( @@ -177,6 +186,7 @@ impl HttpTriggerArgs { self, route_path: &str, called_path: &str, + trigger_path: &str, params: &HashMap, headers: HashMap>, query: HashMap>, @@ -193,6 +203,7 @@ impl HttpTriggerArgs { method: (&self.0.metadata.method).try_into()?, route: route_path, path: called_path, + trigger_path, params, }), ); diff --git a/backend/windmill-trigger-websocket/src/listener.rs b/backend/windmill-trigger-websocket/src/listener.rs index f6fba882a8..8aab61d9c6 100644 --- a/backend/windmill-trigger-websocket/src/listener.rs +++ b/backend/windmill-trigger-websocket/src/listener.rs @@ -322,7 +322,7 @@ impl Listener for WebsocketTrigger { db: &DB, listening_trigger: &ListeningTrigger, payload: Self::Payload, - trigger_info: HashMap>, + mut trigger_info: HashMap>, extra: Option, ) -> Result<()> { let ListeningTrigger { @@ -338,6 +338,7 @@ impl Listener for WebsocketTrigger { let WebsocketConfig { url, .. } = trigger_config; + trigger_info.insert("trigger_path".to_string(), to_raw_value(path)); let args = WebsocketTrigger::build_job_args( &script_path, *is_flow, diff --git a/backend/windmill-trigger/src/listener.rs b/backend/windmill-trigger/src/listener.rs index d09bb5e55a..04e1df102d 100644 --- a/backend/windmill-trigger/src/listener.rs +++ b/backend/windmill-trigger/src/listener.rs @@ -21,6 +21,7 @@ use windmill_common::{ jobs::JobTriggerKind, triggers::{TriggerKind, TriggerMetadata}, utils::report_critical_error, + worker::to_raw_value, DB, INSTANCE_NAME, }; @@ -467,9 +468,13 @@ pub trait Listener: TriggerCrud + TriggerJobArgs { db: &DB, listening_trigger: &ListeningTrigger, payload: Self::Payload, - trigger_info: HashMap>, + mut trigger_info: HashMap>, _extra: Option, ) -> Result<()> { + trigger_info.insert( + "trigger_path".to_string(), + to_raw_value(&listening_trigger.path), + ); let args = Self::build_job_args( &listening_trigger.script_path, listening_trigger.is_flow, @@ -552,6 +557,11 @@ pub trait Listener: TriggerCrud + TriggerJobArgs { return Ok(()); } + let mut trigger_info = trigger_info; + trigger_info.insert( + "trigger_path".to_string(), + to_raw_value(&listening_trigger.path), + ); let (main_args, preprocessor_args) = Self::build_capture_payloads(&payload, trigger_info); if let Err(err) = insert_capture_payload( db, diff --git a/frontend/src/lib/components/triggers/TriggerAdvancedBadges.svelte b/frontend/src/lib/components/triggers/TriggerAdvancedBadges.svelte new file mode 100644 index 0000000000..b261aa8ed7 --- /dev/null +++ b/frontend/src/lib/components/triggers/TriggerAdvancedBadges.svelte @@ -0,0 +1,28 @@ + + +{#if allBadges.length > 0} +
+ {#each allBadges as badge} + {badge.name} + {/each} +
+{/if} diff --git a/frontend/src/lib/components/triggers/email/EmailTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/email/EmailTriggerEditorInner.svelte index b40ef8e505..a33f14243b 100644 --- a/frontend/src/lib/components/triggers/email/EmailTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/email/EmailTriggerEditorInner.svelte @@ -25,6 +25,7 @@ import Tabs from '$lib/components/common/tabs/Tabs.svelte' import Tab from '$lib/components/common/tabs/Tab.svelte' import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte' + import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte' import { saveEmailTriggerFromCfg } from './utils' import { deepEqual } from 'fast-equals' import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte' @@ -370,7 +371,10 @@ />
-
+ {#snippet header()} + + {/snippet} +
@@ -390,6 +394,7 @@
+
{/if} {/snippet} diff --git a/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorInner.svelte index 5e1b08d2c3..6a11d6180b 100644 --- a/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/gcp/GcpTriggerEditorInner.svelte @@ -31,6 +31,7 @@ import Tabs from '$lib/components/common/tabs/Tabs.svelte' import Tab from '$lib/components/common/tabs/Tab.svelte' import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte' + import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte' import Subsection from '$lib/components/Subsection.svelte' import Toggle from '$lib/components/Toggle.svelte' @@ -452,7 +453,12 @@ />
-
+ {#snippet header()} + + {/snippet} +
@@ -520,6 +526,7 @@
+
{/if} {/snippet} diff --git a/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte b/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte index a35227e081..272a0b707c 100644 --- a/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte +++ b/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte @@ -52,6 +52,7 @@ import Tabs from '$lib/components/common/tabs/Tabs.svelte' import Tab from '$lib/components/common/tabs/Tab.svelte' import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte' + import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte' import { deepEqual } from 'fast-equals' import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte' import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte' @@ -695,6 +696,13 @@ {#if !is_static_website}
+ {#snippet header()} + + {/snippet}
@@ -909,6 +917,7 @@
+
{/if}
{/if} diff --git a/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte index ad5cce6f92..5f68f03705 100644 --- a/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte @@ -6,12 +6,7 @@ import Path from '$lib/components/Path.svelte' import Required from '$lib/components/Required.svelte' import ScriptPicker from '$lib/components/ScriptPicker.svelte' - import { - KafkaTriggerService, - type ErrorHandler, - type Retry, - type TriggerMode - } from '$lib/gen' + import { KafkaTriggerService, type ErrorHandler, type Retry, type TriggerMode } from '$lib/gen' import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores' import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils' import Section from '$lib/components/Section.svelte' @@ -25,10 +20,13 @@ import Tabs from '$lib/components/common/tabs/Tabs.svelte' import Tab from '$lib/components/common/tabs/Tab.svelte' import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte' + import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte' import { deepEqual } from 'fast-equals' import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte' import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte' import TriggerFilters from '../TriggerFilters.svelte' + import Select from '$lib/components/select/Select.svelte' + import Toggle from '$lib/components/Toggle.svelte' interface Props { useDrawer?: boolean @@ -87,6 +85,7 @@ let kafkaResourcePath = $state('') let kafkaCfg: Record = $state({}) let autoOffsetReset = $state('latest') + let autoCommit = $state(true) let deploymentLoading = $state(false) let resetLoading = $state(false) let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler') @@ -176,6 +175,7 @@ topics: nDefaultValues?.topics ?? [''] } autoOffsetReset = nDefaultValues?.auto_offset_reset ?? 'latest' + autoCommit = nDefaultValues?.auto_commit ?? true initialScriptPath = '' fixedScriptPath = fixedScriptPath_ ?? '' script_path = fixedScriptPath @@ -207,6 +207,7 @@ topics: cfg?.topics } autoOffsetReset = cfg?.auto_offset_reset ?? 'latest' + autoCommit = cfg?.auto_commit ?? true mode = cfg?.mode ?? 'enabled' extra_perms = cfg?.extra_perms can_write = canWrite(path, cfg?.extra_perms, $userStore) @@ -240,6 +241,7 @@ topics: kafkaCfg.topics, filters, auto_offset_reset: autoOffsetReset, + auto_commit: autoCommit, mode, extra_perms: extra_perms, error_handler_path, @@ -481,36 +483,85 @@ bind:kafkaCfgValid bind:kafkaResourcePath bind:kafkaCfg - bind:autoOffsetReset {path} {can_write} showTestingBadge={isEditor} /> - {#if edit && can_write} - - {/if} - - -
-
+ {#snippet header()} + 0 } + ]} + /> + {/snippet} +
+ -
diff --git a/frontend/src/lib/components/triggers/kafka/utils.ts b/frontend/src/lib/components/triggers/kafka/utils.ts index 16e065093d..12ee49ac24 100644 --- a/frontend/src/lib/components/triggers/kafka/utils.ts +++ b/frontend/src/lib/components/triggers/kafka/utils.ts @@ -25,6 +25,7 @@ export async function saveKafkaTriggerFromCfg( topics: cfg.topics, filters: cfg.filters ?? [], auto_offset_reset: cfg.auto_offset_reset ?? 'latest', + auto_commit: cfg.auto_commit ?? true, ...errorHandlerAndRetries } try { diff --git a/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte index 18148fdbab..df5c7c21e1 100644 --- a/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte @@ -29,6 +29,7 @@ import Tabs from '$lib/components/common/tabs/Tabs.svelte' import Tab from '$lib/components/common/tabs/Tab.svelte' import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte' + import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte' import Toggle from '$lib/components/Toggle.svelte' import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte' @@ -469,7 +470,13 @@ />
-
+ {#snippet header()} + + {/snippet} +
@@ -606,6 +613,7 @@
+
{/if} {/snippet} diff --git a/frontend/src/lib/components/triggers/nats/NatsTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/nats/NatsTriggerEditorInner.svelte index 8c7fbb846c..a3c02c57dc 100644 --- a/frontend/src/lib/components/triggers/nats/NatsTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/nats/NatsTriggerEditorInner.svelte @@ -19,6 +19,7 @@ import Tabs from '$lib/components/common/tabs/Tabs.svelte' import Tab from '$lib/components/common/tabs/Tab.svelte' import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte' + import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte' import { deepEqual } from 'fast-equals' import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte' import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte' @@ -452,7 +453,10 @@ />
-
+ {#snippet header()} + + {/snippet} +
@@ -472,6 +476,7 @@
+
{/if} {/snippet} diff --git a/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditorInner.svelte index cd176ac51c..5ffe489a22 100644 --- a/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/postgres/PostgresTriggerEditorInner.svelte @@ -36,6 +36,7 @@ import TestingBadge from '../testingBadge.svelte' import { getHandlerType, handleConfigChange, type Trigger } from '../utils' import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte' + import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte' import { fade } from 'svelte/transition' import MultiSelect from '$lib/components/select/MultiSelect.svelte' import { safeSelectItems } from '$lib/components/select/utils.svelte' @@ -854,7 +855,10 @@
-
+ {#snippet header()} + + {/snippet} +
@@ -874,6 +878,7 @@
+
{/if} {/snippet} diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte index 4aca94c4b5..55f825b40c 100644 --- a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte @@ -1,5 +1,6 @@ -
+
Ducklake
diff --git a/frontend/src/lib/components/workspaceSettings/StorageSettings.svelte b/frontend/src/lib/components/workspaceSettings/StorageSettings.svelte index 44b9d5d1d3..ef46189444 100644 --- a/frontend/src/lib/components/workspaceSettings/StorageSettings.svelte +++ b/frontend/src/lib/components/workspaceSettings/StorageSettings.svelte @@ -102,19 +102,6 @@ let hasUnsavedChanges = $derived.by(() => { return !deepEqual(s3ResourceSettings, s3ResourceSavedSettings) }) - - let volumeStorageItems: { value: string; label: string }[] = $derived.by(() => { - const items: { value: string; label: string }[] = [{ value: '', label: 'Disabled' }] - if (!emptyString(s3ResourceSettings.resourcePath)) { - items.push({ value: 'primary', label: 'Primary storage' }) - } - for (const [name, s] of s3ResourceSettings.secondaryStorage ?? []) { - if (!emptyString(s.resourcePath)) { - items.push({ value: name, label: name }) - } - } - return items - }) @@ -236,18 +223,18 @@ class="cursor-not-allowed" > {#snippet trigger()} - + - + {/snippet} {#snippet content()} - + {#if emptyString(tableRow[1].resourcePath)} Please select a storage resource {:else if isDirty(tableRow[0])} Please save your changes {/if} - + {/snippet} {:else} @@ -330,25 +317,6 @@ -
- -
- s3ResourceSettings.volumeStorage ?? '', + (v) => { + s3ResourceSettings.volumeStorage = v || undefined + } + } + /> +
+ + onDiscard?.()} + saveLabel="Save volume storage settings" + /> + {:else} + + You need to configure a workspace object storage before you can use volumes. + + + {/if} +{/if} diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index 8857b170c5..593a361b57 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -51,6 +51,7 @@ import ConnectionSection from '$lib/components/ConnectionSection.svelte' import AISettings from '$lib/components/workspaceSettings/AISettings.svelte' import StorageSettings from '$lib/components/workspaceSettings/StorageSettings.svelte' + import VolumeStorageSettings from '$lib/components/workspaceSettings/VolumeStorageSettings.svelte' import GitSyncSection from '$lib/components/git_sync/GitSyncSection.svelte' import { untrack } from 'svelte' import { getHandlerType } from '$lib/components/triggers/utils' @@ -295,6 +296,8 @@ | 'ai' | 'windmill_data_tables' | 'windmill_lfs' + | 'volume_storage' + | 'ducklake' | 'git_sync' | 'default_app' | 'native_triggers' @@ -844,22 +847,40 @@ // Function to check if there are unsaved changes in storage settings function getStorageSettingsInitialAndModifiedValues() { - const savedValue = { - s3ResourceSettings: s3ResourceSavedSettings, - ducklakeSettings: ducklakeSavedSettings + return { + savedValue: { s3ResourceSettings: s3ResourceSavedSettings }, + modifiedValue: { s3ResourceSettings: s3ResourceSettings } } - - const modifiedValue = { - s3ResourceSettings: s3ResourceSettings, - ducklakeSettings: ducklakeSettings - } - - return { savedValue, modifiedValue } } // Function to discard unsaved storage settings changes function discardStorageSettingsChanges() { s3ResourceSettings = clone(s3ResourceSavedSettings) + } + + // Function to check if there are unsaved changes in volume storage settings + function getVolumeStorageInitialAndModifiedValues() { + return { + savedValue: { volumeStorage: s3ResourceSavedSettings.volumeStorage }, + modifiedValue: { volumeStorage: s3ResourceSettings.volumeStorage } + } + } + + // Function to discard unsaved volume storage changes + function discardVolumeStorageChanges() { + s3ResourceSettings.volumeStorage = s3ResourceSavedSettings.volumeStorage + } + + // Function to check if there are unsaved changes in ducklake settings + function getDucklakeSettingsInitialAndModifiedValues() { + return { + savedValue: { ducklakeSettings: ducklakeSavedSettings }, + modifiedValue: { ducklakeSettings: ducklakeSettings } + } + } + + // Function to discard unsaved ducklake settings changes + function discardDucklakeSettingsChanges() { ducklakeSettings = clone(ducklakeSavedSettings) } @@ -998,6 +1019,10 @@ return getAiSettingsInitialAndModifiedValues() case 'windmill_lfs': return getStorageSettingsInitialAndModifiedValues() + case 'volume_storage': + return getVolumeStorageInitialAndModifiedValues() + case 'ducklake': + return getDucklakeSettingsInitialAndModifiedValues() case 'deploy_to': return getDeploySettingsInitialAndModifiedValues() case 'webhook': @@ -1038,6 +1063,12 @@ case 'windmill_lfs': discardStorageSettingsChanges() break + case 'volume_storage': + discardVolumeStorageChanges() + break + case 'ducklake': + discardDucklakeSettingsChanges() + break case 'deploy_to': discardDeploySettingsChanges() break @@ -1178,6 +1209,18 @@ label: 'Object storage (S3)', aiId: 'workspace-settings-windmill-lfs', aiDescription: 'Object Storage (S3) workspace settings' + }, + { + id: 'volume_storage', + label: 'Volumes', + aiId: 'workspace-settings-volume-storage', + aiDescription: 'Volume storage workspace settings' + }, + { + id: 'ducklake', + label: 'Ducklake', + aiId: 'workspace-settings-ducklake', + aiDescription: 'Ducklake workspace settings' } ] }, @@ -1811,6 +1854,18 @@ export async function main( s3ResourceSettings = clone(s3ResourceSavedSettings) }} /> + {:else if tab == 'volume_storage'} + { + s3ResourceSavedSettings = clone(s3ResourceSettings) + }} + onDiscard={() => { + s3ResourceSettings = clone(s3ResourceSavedSettings) + }} + /> + {:else if tab == 'ducklake'} Date: Fri, 13 Mar 2026 07:08:33 +0100 Subject: [PATCH 17/25] add a hint to commit message on git sync for renames (#8343) * add a hint to commit message on git sync for renames * chore: update ee-repo-ref to 344e2aa60d8cafe08b8c57445d2f9555bad7625a This commit updates the EE repository reference after PR #458 was merged in windmill-ee-private. Previous ee-repo-ref: ad4c90cc96ad1b4511cba6b6ee28351895882156 New ee-repo-ref: 344e2aa60d8cafe08b8c57445d2f9555bad7625a Automated by sync-ee-ref workflow. --------- Co-authored-by: windmill-internal-app[bot] --- backend/ee-repo-ref.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index a048a9b463..ee34a47736 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -fcd3ea52b0cc94fbe1159baf662a38da947456de \ No newline at end of file +344e2aa60d8cafe08b8c57445d2f9555bad7625a From 4c2c165a5b757bd5f2f49074bb290407bce3b2fb Mon Sep 17 00:00:00 2001 From: Pyra <92104930+pyranota@users.noreply.github.com> Date: Fri, 13 Mar 2026 07:10:28 +0100 Subject: [PATCH 18/25] feat(cli): add unified generate-metadata command (#8335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cli): add unified generate-metadata command - Add generate-metadata command that calls script, flow, and app handlers - Export generateLocks from flow.ts and generateMetadata from script.ts - Add deprecation warnings to individual metadata commands * feat(cli): improve unified generate-metadata command - Use internal handlers for single-pass collection of stale items - Add --dry-run flag to show what would be updated - Fix WASM parser init deprecation warning - Add comprehensive tests for all flags - Match original handler behavior for per-item messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * feat(cli): add skip flags and comprehensive tests for generate-metadata - Add --skip-scripts, --skip-flows, --skip-apps flags for granular control - --schema-only now properly skips flows and apps (they only have locks) - Dynamic "Checking X, Y, Z..." message based on what's being processed - Show warning when all types are skipped - Add comprehensive tests for all flags: - --dry-run shows stale items without updating - --schema-only only processes scripts - --skip-scripts, --skip-flows, --skip-apps work correctly - skipping all types shows warning - 'All metadata up-to-date' when nothing to update 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * improve output Signed-off-by: pyranota * refactor(cli): add shared test fixtures with cross-links Co-Authored-By: Claude Opus 4.5 * feat(cli): add folder argument to generate-metadata command Co-Authored-By: Claude Opus 4.5 --------- Signed-off-by: pyranota Co-authored-by: Claude Opus 4.5 --- cli/src/commands/app/app.ts | 3 + cli/src/commands/app/app_metadata.ts | 18 +- cli/src/commands/flow/flow.ts | 5 +- cli/src/commands/flow/flow_metadata.ts | 10 +- .../generate-metadata/generate-metadata.ts | 332 +++++++++++ cli/src/commands/script/script.ts | 5 +- cli/src/main.ts | 2 + cli/src/utils/metadata.ts | 4 +- cli/test/sync_pull_push.test.ts | 23 +- cli/test/test_backend.ts | 13 + cli/test/test_fixtures.ts | 531 ++++++++++++++++++ cli/test/unified_generate_metadata.test.ts | 451 +++++++++++++++ 12 files changed, 1378 insertions(+), 19 deletions(-) create mode 100644 cli/src/commands/generate-metadata/generate-metadata.ts create mode 100644 cli/test/test_fixtures.ts create mode 100644 cli/test/unified_generate_metadata.test.ts diff --git a/cli/src/commands/app/app.ts b/cli/src/commands/app/app.ts index 3aa8305542..f048aac616 100644 --- a/cli/src/commands/app/app.ts +++ b/cli/src/commands/app/app.ts @@ -275,6 +275,9 @@ const command = new Command() "Default TypeScript runtime (bun or deno)" ) .action(async (opts: any, appFolder: string | undefined) => { + log.warn( + colors.yellow('This command is deprecated. Use "wmill generate-metadata" instead.') + ); const { generateLocksCommand } = await import("./app_metadata.ts"); await generateLocksCommand(opts, appFolder); }); diff --git a/cli/src/commands/app/app_metadata.ts b/cli/src/commands/app/app_metadata.ts index 3a28770225..78a3019566 100644 --- a/cli/src/commands/app/app_metadata.ts +++ b/cli/src/commands/app/app_metadata.ts @@ -157,7 +157,7 @@ export async function generateAppLocksInternal( return remote_path; } - if (Object.keys(filteredDeps).length > 0) { + if (Object.keys(filteredDeps).length > 0 && !noStaleMessage) { log.info( (await blueColor())( `Found workspace dependencies (${workspaceDependenciesLanguages @@ -180,9 +180,11 @@ export async function generateAppLocksInternal( } if (changedScripts.length > 0) { - log.info( - `Recomputing locks of ${changedScripts.join(", ")} in ${appFolder}` - ); + if (!noStaleMessage) { + log.info( + `Recomputing locks of ${changedScripts.join(", ")} in ${appFolder}` + ); + } if (rawApp) { const runnablesPath = path.join(appFolder, APP_BACKEND_FOLDER); @@ -230,7 +232,7 @@ export async function generateAppLocksInternal( yamlStringify(appFile as Record, yamlOptions) ); } - } else { + } else if (!noStaleMessage) { log.info(colors.gray(`No scripts changed in ${appFolder}`)); } } @@ -246,7 +248,9 @@ export async function generateAppLocksInternal( for (const [scriptPath, hash] of Object.entries(hashes)) { await updateMetadataGlobalLock(appFolder, hash, scriptPath); } - log.info(colors.green(`App ${remote_path} lockfiles updated`)); + if (!noStaleMessage) { + log.info(colors.green(`App ${remote_path} lockfiles updated`)); + } } /** @@ -767,7 +771,7 @@ export async function inferRunnableSchemaFromFile( } } -function getAppFolders(elems: Record, extension: string) { +export function getAppFolders(elems: Record, extension: string) { return Object.keys(elems) .filter((p) => p.endsWith(SEP + extension)) .map((p) => p.substring(0, p.length - (SEP + extension).length)); diff --git a/cli/src/commands/flow/flow.ts b/cli/src/commands/flow/flow.ts index f0974cf354..3b01385fb9 100644 --- a/cli/src/commands/flow/flow.ts +++ b/cli/src/commands/flow/flow.ts @@ -308,12 +308,15 @@ async function preview( } } -async function generateLocks( +export async function generateLocks( opts: GlobalOptions & { yes?: boolean; } & SyncOptions, folder: string | undefined ) { + log.warn( + colors.yellow('This command is deprecated. Use "wmill generate-metadata" instead.') + ); const workspace = await resolveWorkspace(opts); await requireLogin(opts); opts = await mergeConfigWithConfigFile(opts); diff --git a/cli/src/commands/flow/flow_metadata.ts b/cli/src/commands/flow/flow_metadata.ts index d52d05a3ed..2e48efaa10 100644 --- a/cli/src/commands/flow/flow_metadata.ts +++ b/cli/src/commands/flow/flow_metadata.ts @@ -98,7 +98,7 @@ export async function generateFlowLockInternal( return remote_path; } - if (Object.keys(filteredDeps).length > 0) { + if (Object.keys(filteredDeps).length > 0 && !noStaleMessage) { log.info( (await blueColor())( `Found workspace dependencies (${workspaceDependenciesLanguages @@ -121,7 +121,9 @@ export async function generateFlowLockInternal( } } - log.info(`Recomputing locks of ${changedScripts.join(", ")} in ${folder}`); + if (!noStaleMessage) { + log.info(`Recomputing locks of ${changedScripts.join(", ")} in ${folder}`); + } const fileReader = async (path: string) => await readFile(folder + SEP + path, "utf-8"); await replaceInlineScripts( flowValue.value.modules, @@ -180,7 +182,9 @@ export async function generateFlowLockInternal( for (const [path, hash] of Object.entries(hashes)) { await updateMetadataGlobalLock(folder, hash, path); } - log.info(colors.green(`Flow ${remote_path} lockfiles updated`)); + if (!noStaleMessage) { + log.info(colors.green(`Flow ${remote_path} lockfiles updated`)); + } } /** diff --git a/cli/src/commands/generate-metadata/generate-metadata.ts b/cli/src/commands/generate-metadata/generate-metadata.ts new file mode 100644 index 0000000000..0d7435856c --- /dev/null +++ b/cli/src/commands/generate-metadata/generate-metadata.ts @@ -0,0 +1,332 @@ +import { Command } from "@cliffy/command"; +import { Confirm } from "@cliffy/prompt/confirm"; +import { colors } from "@cliffy/ansi/colors"; +import { sep as SEP } from "node:path"; +import { GlobalOptions } from "../../types.ts"; +import { SyncOptions, mergeConfigWithConfigFile } from "../../core/conf.ts"; +import { resolveWorkspace } from "../../core/context.ts"; +import { requireLogin } from "../../core/auth.ts"; +import * as log from "../../core/log.ts"; +import { + generateScriptMetadataInternal, + getRawWorkspaceDependencies, +} from "../../utils/metadata.ts"; +import { generateFlowLockInternal } from "../flow/flow_metadata.ts"; +import { generateAppLocksInternal, getAppFolders } from "../app/app_metadata.ts"; +import { + elementsToMap, + FSFSElement, + ignoreF, +} from "../sync/sync.ts"; +import { exts } from "../script/script.ts"; +import { isFlowPath, isAppPath } from "../../utils/resource_folders.ts"; +import { listSyncCodebases } from "../../utils/codebase.ts"; + +interface StaleItem { + type: "script" | "flow" | "app"; + path: string; + folder: string; + isRawApp?: boolean; +} + +async function generateMetadata( + opts: GlobalOptions & { + yes?: boolean; + lockOnly?: boolean; + schemaOnly?: boolean; + dryRun?: boolean; + skipScripts?: boolean; + skipFlows?: boolean; + skipApps?: boolean; + } & SyncOptions, + folder?: string +) { + if (folder === "") { + folder = undefined; + } + + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + opts = await mergeConfigWithConfigFile(opts); + + const rawWorkspaceDependencies = await getRawWorkspaceDependencies(); + const codebases = await listSyncCodebases(opts); + const ignore = await ignoreF(opts); + + const staleItems: StaleItem[] = []; + + // --schema-only implies skipping flows and apps (they only have locks, no schemas) + const skipScripts = opts.skipScripts ?? false; + const skipFlows = opts.skipFlows ?? opts.schemaOnly ?? false; + const skipApps = opts.skipApps ?? opts.schemaOnly ?? false; + + const checking: string[] = []; + if (!skipScripts) checking.push("scripts"); + if (!skipFlows) checking.push("flows"); + if (!skipApps) checking.push("apps"); + + if (checking.length === 0) { + log.info(colors.yellow("Nothing to check (all types skipped)")); + return; + } + + log.info(colors.gray(`Checking ${checking.join(", ")}...`)); + + // === Collect stale scripts === + if (!skipScripts) { + // TODO: run elementsToMap only once but for all runnable types. + const scriptElems = await elementsToMap( + await FSFSElement(process.cwd(), codebases, false), + (p, isD) => { + return ( + (!isD && !exts.some((ext) => p.endsWith(ext))) || + ignore(p, isD) || + isFlowPath(p) || + isAppPath(p) + ); + }, + false, + {} + ); + + for (const e of Object.keys(scriptElems)) { + const candidate = await generateScriptMetadataInternal( + e, + workspace, + opts, + true, // dryRun + true, // noStaleMessage + rawWorkspaceDependencies, + codebases, + false + ); + if (candidate) { + staleItems.push({ type: "script", path: candidate, folder: e }); + } + } + } + + // === Collect stale flows === + if (!skipFlows) { + const flowElems = Object.keys( + await elementsToMap( + await FSFSElement(process.cwd(), [], true), + (p, isD) => { + return ( + ignore(p, isD) || + (!isD && + !p.endsWith(SEP + "flow.yaml") && + !p.endsWith(SEP + "flow.json")) + ); + }, + false, + {} + ) + ).map((x) => x.substring(0, x.lastIndexOf(SEP))); + + for (const folder of flowElems) { + const candidate = await generateFlowLockInternal( + folder, + true, // dryRun + workspace, + opts, + false, + true // noStaleMessage + ); + if (candidate) { + staleItems.push({ type: "flow", path: candidate, folder }); + } + } + } + + // === Collect stale apps === + if (!skipApps) { + const elems = await elementsToMap( + await FSFSElement(process.cwd(), [], true), + (p, isD) => { + return ( + ignore(p, isD) || + (!isD && + !p.endsWith(SEP + "raw_app.yaml") && + !p.endsWith(SEP + "app.yaml")) + ); + }, + false, + {} + ); + + const rawAppFolders = getAppFolders(elems, "raw_app.yaml"); + const appFolders = getAppFolders(elems, "app.yaml"); + + for (const appFolder of rawAppFolders) { + const candidate = await generateAppLocksInternal( + appFolder, + true, // rawApp + true, // dryRun + workspace, + opts, + false, + true // noStaleMessage + ); + if (candidate) { + staleItems.push({ type: "app", path: candidate, folder: appFolder, isRawApp: true }); + } + } + + for (const appFolder of appFolders) { + const candidate = await generateAppLocksInternal( + appFolder, + false, // rawApp + true, // dryRun + workspace, + opts, + false, + true // noStaleMessage + ); + if (candidate) { + staleItems.push({ type: "app", path: candidate, folder: appFolder, isRawApp: false }); + } + } + } + + // === Filter by folder if specified === + let filteredItems = staleItems; + if (folder) { + // Strip trailing separator to match deprecated flow/app handler behavior + // (see generateFlowLockInternal line 64-66, generateAppLocksInternal line 109-110) + if (folder.endsWith(SEP)) { + folder = folder.substring(0, folder.length - 1); + } + filteredItems = staleItems.filter((item) => item.folder === folder || item.folder.startsWith(folder + SEP)); + } + + // === Show stale items and confirm === + if (filteredItems.length === 0) { + log.info(colors.green("All metadata up-to-date")); + return; + } + + // Group items by type for display + const scripts = filteredItems.filter((i) => i.type === "script"); + const flows = filteredItems.filter((i) => i.type === "flow"); + const apps = filteredItems.filter((i) => i.type === "app"); + + log.info(""); + log.info(`Found ${filteredItems.length} item(s) with stale metadata:`); + + if (scripts.length > 0) { + log.info(colors.gray(` Scripts (${scripts.length}):`)); + for (const item of scripts) { + log.info(colors.yellow(` ${item.path}`)); + } + } + if (flows.length > 0) { + log.info(colors.gray(` Flows (${flows.length}):`)); + for (const item of flows) { + log.info(colors.yellow(` ${item.path}`)); + } + } + if (apps.length > 0) { + log.info(colors.gray(` Apps (${apps.length}):`)); + for (const item of apps) { + log.info(colors.yellow(` ${item.path}`)); + } + } + + if (opts.dryRun) { + return; + } + + log.info(""); + + if ( + !opts.yes && + !(await Confirm.prompt({ + message: "Update metadata?", + default: true, + })) + ) { + return; + } + + log.info(""); + + // === Process all stale items with progress counter === + const total = filteredItems.length; + const maxWidth = `[${total}/${total}]`.length; + let current = 0; + + const formatProgress = (n: number) => { + const bracket = `[${n}/${total}]`; + return colors.gray(bracket.padEnd(maxWidth, " ")); + }; + + // Process scripts + for (const item of scripts) { + current++; + log.info(`${formatProgress(current)} script ${colors.cyan(item.path)}`); + await generateScriptMetadataInternal( + item.folder, + workspace, + opts, + false, // dryRun + true, // noStaleMessage - we handle output + rawWorkspaceDependencies, + codebases, + false + ); + } + + // Process flows + for (const item of flows) { + current++; + log.info(`${formatProgress(current)} flow ${colors.cyan(item.path)}`); + await generateFlowLockInternal( + item.folder, + false, // dryRun + workspace, + opts, + false, + true // noStaleMessage - we handle output + ); + } + // Process apps + for (const item of apps) { + current++; + log.info(`${formatProgress(current)} app ${colors.cyan(item.path)}`); + await generateAppLocksInternal( + item.folder, + item.isRawApp!, // rawApp + false, // dryRun + workspace, + opts, + false, + true // noStaleMessage - we handle output + ); + } + + log.info(""); + log.info(colors.green(`Done. Updated ${total} item(s).`)); +} + +const command = new Command() + .description("Generate metadata (locks, schemas) for all scripts, flows, and apps") + .arguments("[folder:string]") + .option("--yes", "Skip confirmation prompt") + .option("--dry-run", "Show what would be updated without making changes") + .option("--lock-only", "Re-generate only the lock files") + .option("--schema-only", "Re-generate only script schemas (skips flows and apps)") + .option("--skip-scripts", "Skip processing scripts") + .option("--skip-flows", "Skip processing flows") + .option("--skip-apps", "Skip processing apps") + .option( + "-i --includes ", + "Comma separated patterns to specify which files to include" + ) + .option( + "-e --excludes ", + "Comma separated patterns to specify which files to exclude" + ) + .action(generateMetadata as any); + +export default command; diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 9c6f094b41..3a6556b34d 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -978,7 +978,7 @@ export type GlobalDeps = Map< Record >; -async function generateMetadata( +export async function generateMetadata( opts: GlobalOptions & { lockOnly?: boolean; schemaOnly?: boolean; @@ -986,6 +986,9 @@ async function generateMetadata( } & SyncOptions, scriptPath: string | undefined ) { + log.warn( + colors.yellow('This command is deprecated. Use "wmill generate-metadata" instead.') + ); log.info( "This command only works for workspace scripts, for flows inline scripts use `wmill flow generate-locks`" ); diff --git a/cli/src/main.ts b/cli/src/main.ts index f923ef38fd..d81570e784 100755 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -39,6 +39,7 @@ import queues from "./commands/queues/queues.ts"; import dependencies from "./commands/dependencies/dependencies.ts"; import init from "./commands/init/init.ts"; import jobs from "./commands/jobs/jobs.ts"; +import generateMetadata from "./commands/generate-metadata/generate-metadata.ts"; import docs from "./commands/docs/docs.ts"; import { fetchVersion } from "./core/context.ts"; @@ -129,6 +130,7 @@ const command = new Command() .command("queues", queues) .command("dependencies", dependencies) .command("jobs", jobs) + .command("generate-metadata", generateMetadata) .command("docs", docs) .command("version --version", "Show version information") .action(async (opts: any) => { diff --git a/cli/src/utils/metadata.ts b/cli/src/utils/metadata.ts index 590c3d7d8e..76ff33247a 100644 --- a/cli/src/utils/metadata.ts +++ b/cli/src/utils/metadata.ts @@ -35,7 +35,7 @@ function loadParser(pkgName: string): Promise { const wasmPath = _require.resolve( `${pkgName}/windmill_parser_wasm_bg.wasm` ); - await mod.default(readFileSync(wasmPath)); + await mod.default({ module_or_path: readFileSync(wasmPath) }); return mod; })(); _parserCache.set(pkgName, p); @@ -223,7 +223,7 @@ export async function generateScriptMetadataInternal( return `${remotePath} (${language})`; } - if (!justUpdateMetadataLock) { + if (!justUpdateMetadataLock && !noStaleMessage) { log.info(colors.gray(`Generating metadata for ${scriptPath}`)); } diff --git a/cli/test/sync_pull_push.test.ts b/cli/test/sync_pull_push.test.ts index 2b062eb157..613de2ef82 100644 --- a/cli/test/sync_pull_push.test.ts +++ b/cli/test/sync_pull_push.test.ts @@ -3,6 +3,13 @@ * * Tests the sync pull and push functionality with a simulated filesystem * containing every kind of Windmill resource type. + * + * CROSS-LINKS - Related test helper locations (keep in sync when adding new helpers): + * @see test_fixtures.ts - Shared local fixtures (prefer using this module for new tests) + * @see test_backend.ts - API-based creation helpers (createTestApp, createTestResource, etc.) + * + * This file contains: Local fixtures (should migrate to test_fixtures.ts) + createRemoteScript + * If you add new helpers, update cross-links in the files above. */ import { expect, test, describe } from "bun:test"; @@ -37,10 +44,13 @@ import { newPathAssigner } from "../windmill-utils-internal/src/path-utils/path- // ============================================================================= // Test Fixtures - Every Type of Windmill Resource +// See file header for cross-links to related helpers. +// Consider migrating these to test_fixtures.ts for reuse across tests. // ============================================================================= /** - * Creates a mock script file structure + * Creates a mock script file structure. + * See file header for cross-links to related helpers. */ function createScriptFixture( name: string, @@ -89,7 +99,8 @@ kind: script } /** - * Creates a mock flow file structure + * Creates a mock flow file structure. + * See file header for cross-links to related helpers. */ function createFlowFixture(name: string): Record { const flowSuffix = getFolderSuffix("flow"); @@ -123,7 +134,8 @@ schema: } /** - * Creates a mock app file structure + * Creates a mock app file structure. + * See file header for cross-links to related helpers. */ function createAppFixture(name: string): Record { const appSuffix = getFolderSuffix("app"); @@ -151,7 +163,8 @@ policy: } /** - * Creates a mock raw_app file structure + * Creates a mock raw_app file structure. + * See file header for cross-links to related helpers. */ function createRawAppFixture(name: string): Record { const rawAppSuffix = getFolderSuffix("raw_app"); @@ -1920,7 +1933,7 @@ excludes: [] import type { TestBackend } from "./test_backend.ts"; -/** Create a script on the remote via API */ +/** Create a script on the remote via API. See file header for cross-links. */ async function createRemoteScript( backend: TestBackend, scriptPath: string, diff --git a/cli/test/test_backend.ts b/cli/test/test_backend.ts index 63c7f3078d..16fb79528d 100644 --- a/cli/test/test_backend.ts +++ b/cli/test/test_backend.ts @@ -19,6 +19,13 @@ * // ... * }); * }); + * + * CROSS-LINKS - Related test helper locations (keep in sync when adding new helpers): + * @see test_fixtures.ts - Local file fixtures (createLocalScript, createLocalFlow, etc.) + * @see sync_pull_push.test.ts - Local fixtures + createRemoteScript (API-based) + * + * This file contains: API-based creation helpers (createTestApp, createTestResource, etc.) + * If you add new helpers, update cross-links in the files above. */ import { CargoBackend, CargoBackendConfig } from "./cargo_backend.ts"; @@ -109,6 +116,7 @@ class CargoBackendAdapter implements TestBackend { return this.backend.apiRequest(path, options); } + /** Seeds test data via API calls. See file header for cross-links to related helpers. */ async seedTestData(): Promise { // Create test folder first await this.createTestFolder("test"); @@ -124,6 +132,7 @@ class CargoBackendAdapter implements TestBackend { await this.createTestApp("f/test/test_dashboard"); } + /** See file header for cross-links to related helpers. */ private async createTestApp(path: string): Promise { const response = await this.backend.apiRequest(`/api/w/${this.workspace}/apps/create`, { method: "POST", @@ -156,6 +165,7 @@ class CargoBackendAdapter implements TestBackend { } } + /** See file header for cross-links to related helpers. */ private async createTestFolder(name: string): Promise { const response = await this.backend.apiRequest(`/api/w/${this.workspace}/folders/create`, { method: "POST", @@ -172,6 +182,7 @@ class CargoBackendAdapter implements TestBackend { } } + /** See file header for cross-links to related helpers. */ private async createTestGroup(name: string): Promise { const response = await this.backend.apiRequest(`/api/w/${this.workspace}/groups/create`, { method: "POST", @@ -189,6 +200,7 @@ class CargoBackendAdapter implements TestBackend { } + /** See file header for cross-links to related helpers. */ private async createTestResource(path: string, description: string): Promise { // First ensure the folder exists const folderPath = path.split("/").slice(0, 2).join("/"); // e.g., "f/test" @@ -228,6 +240,7 @@ class CargoBackendAdapter implements TestBackend { } } + /** See file header for cross-links to related helpers. */ private async createTestVariable(path: string, value: string): Promise { const response = await this.backend.apiRequest( `/api/w/${this.workspace}/variables/create`, diff --git a/cli/test/test_fixtures.ts b/cli/test/test_fixtures.ts new file mode 100644 index 0000000000..5f64869949 --- /dev/null +++ b/cli/test/test_fixtures.ts @@ -0,0 +1,531 @@ +/** + * Test Fixtures + * + * Shared helpers for creating test data (scripts, flows, apps, raw apps) in tests. + * + * Two types of helpers: + * - Fixture functions: Return data structures with paths and contents (no disk I/O) + * - Local creation functions: Create fixtures AND write them to disk + * + * CROSS-LINKS - Related test helper locations (keep in sync when adding new helpers): + * @see test_backend.ts - API-based creation helpers (createTestApp, createTestResource, etc.) + * @see sync_pull_push.test.ts - Local fixtures + createRemoteScript (API-based) + * + * This file contains: Shared local fixtures (createLocalScript, createLocalFlow, etc.) + * If you add new helpers, update cross-links in the files above. + * + * @example + * // Using fixtures (data only) + * const fixture = createScriptFixture("my_script", "bun"); + * + * // Using local creation (writes to disk) + * await createLocalScript(tempDir, "f/test", "my_script", "bun"); + * + * @keywords createLocal, local script, local flow, local app, raw app, fixture, test data + */ + +import { writeFile, mkdir } from "node:fs/promises"; +import { + getFolderSuffix, + getMetadataFileName, +} from "../src/utils/resource_folders.ts"; + +// ============================================================================= +// Fixture Types +// ============================================================================= + +export interface FileFixture { + path: string; + content: string; +} + +export interface ScriptFixture { + contentFile: FileFixture; + metadataFile: FileFixture; +} + +export interface FlowFixture { + metadata: FileFixture; + inlineScript: FileFixture; +} + +export interface AppFixture { + metadata: FileFixture; +} + +export interface RawAppFixture { + metadata: FileFixture; + indexHtml: FileFixture; + indexJs: FileFixture; + [key: string]: FileFixture; +} + +// ============================================================================= +// Script Fixtures +// ============================================================================= + +/** + * Creates a script fixture (data structure, no disk I/O). + * See file header for cross-links to related helpers. + * + * Use this when you need fine-grained control over the script structure. + * For simple cases, use {@link createLocalScript} instead. + * + * @param name - Script name (without extension) + * @param language - Script language + * @param content - Optional custom script content + * @returns Script fixture with content and metadata files + * + * @example + * const fixture = createScriptFixture("my_script", "bun"); + * const fixture = createScriptFixture("custom", "python3", "def main(): return 42"); + * + * @keywords script fixture, create script, local script + */ +export function createScriptFixture( + name: string, + language: "python3" | "deno" | "bun" | "bash" | "go" | "postgresql" = "bun", + content?: string +): ScriptFixture { + const extensions: Record = { + python3: ".py", + deno: ".ts", + bun: ".ts", + bash: ".sh", + go: ".go", + postgresql: ".sql", + }; + + const ext = extensions[language]; + const defaultContent: Record = { + python3: `def main():\n return "Hello from ${name}"`, + deno: `export async function main() {\n return "Hello from ${name}";\n}`, + bun: `export async function main() {\n return "Hello from ${name}";\n}`, + bash: `#!/bin/bash\necho "Hello from ${name}"`, + go: `package inner\n\nfunc main() string {\n return "Hello from ${name}"\n}`, + postgresql: `-- ${name}\nSELECT 'Hello from ${name}';`, + }; + + return { + contentFile: { + path: `${name}${ext}`, + content: content ?? defaultContent[language], + }, + metadataFile: { + path: `${name}.script.yaml`, + content: `summary: "${name} script" +description: "A ${language} script for testing" +schema: + $schema: "https://json-schema.org/draft/2020-12/schema" + type: object + properties: {} + required: [] +is_template: false +lock: "" +kind: script +`, + }, + }; +} + +// ============================================================================= +// Flow Fixtures +// ============================================================================= + +/** + * Creates a flow fixture (data structure, no disk I/O). + * See file header for cross-links to related helpers. + * + * TODO: Add optional params: language, summary, description + * + * Use this when you need fine-grained control over the flow structure. + * For simple cases, use {@link createLocalFlow} instead. + * + * @param name - Flow name + * @param inlineScriptContent - Optional custom inline script content + * @returns Flow fixture with metadata and inline script + * + * @example + * const fixture = createFlowFixture("my_flow"); + * + * @keywords flow fixture, create flow, local flow + */ +export function createFlowFixture( + name: string, + inlineScriptContent?: string +): FlowFixture { + const flowSuffix = getFolderSuffix("flow"); + const metadataFile = getMetadataFileName("flow", "yaml"); + + const scriptContent = + inlineScriptContent ?? + `export async function main() {\n return "Hello from flow ${name}";\n}`; + + return { + metadata: { + path: `${name}${flowSuffix}/${metadataFile}`, + content: `summary: "${name} flow" +description: "A flow for testing" +value: + modules: + - id: a + value: + type: rawscript + content: | + ${scriptContent.split("\n").join("\n ")} + language: bun + input_transforms: {} +schema: + $schema: "https://json-schema.org/draft/2020-12/schema" + type: object + properties: {} + required: [] +`, + }, + inlineScript: { + path: `${name}${flowSuffix}/a.inline_script.ts`, + content: scriptContent, + }, + }; +} + +// ============================================================================= +// App Fixtures +// ============================================================================= + +/** + * Creates an app fixture (data structure, no disk I/O). + * See file header for cross-links to related helpers. + * + * TODO: Add optional params: inlineScriptContent, summary, grid + * + * Use this when you need fine-grained control over the app structure. + * For simple cases, use {@link createLocalApp} instead. + * + * @param name - App name + * @returns App fixture with metadata + * + * @example + * const fixture = createAppFixture("my_app"); + * + * @keywords app fixture, create app, local app + */ +export function createAppFixture(name: string): AppFixture { + const appSuffix = getFolderSuffix("app"); + const metadataFile = getMetadataFileName("app", "yaml"); + + return { + metadata: { + path: `${name}${appSuffix}/${metadataFile}`, + content: `summary: "${name} app" +value: + type: app + grid: + - id: button1 + data: + type: buttoncomponent + componentInput: + type: runnable + runnable: + type: runnableByName + inlineScript: + content: | + export async function main() { + return "hello from app"; + } + language: bun + hiddenInlineScripts: [] + css: {} + norefreshbar: false +policy: + on_behalf_of: null + on_behalf_of_email: null + triggerables: {} + execution_mode: viewer +`, + }, + }; +} + +// ============================================================================= +// Raw App Fixtures +// ============================================================================= + +/** + * Creates a raw app fixture (data structure, no disk I/O). + * See file header for cross-links to related helpers. + * + * TODO: Add optional params: inlineScriptContent, htmlContent, jsContent + * + * Raw apps are React/frontend apps with separate inline scripts. + * Use this when you need fine-grained control over the raw app structure. + * For simple cases, use {@link createLocalRawApp} instead. + * + * @param name - Raw app name + * @returns Raw app fixture with metadata and frontend files + * + * @example + * const fixture = createRawAppFixture("my_raw_app"); + * + * @keywords raw app fixture, create raw app, local raw app, react app + */ +export function createRawAppFixture(name: string): RawAppFixture { + const rawAppSuffix = getFolderSuffix("raw_app"); + const metadataFile = getMetadataFileName("raw_app", "yaml"); + + return { + metadata: { + path: `${name}${rawAppSuffix}/${metadataFile}`, + content: `summary: "${name} raw app" +policy: + execution_mode: publisher + triggerables: {} +`, + }, + indexHtml: { + path: `${name}${rawAppSuffix}/index.html`, + content: ` + +${name} +
+`, + }, + indexJs: { + path: `${name}${rawAppSuffix}/index.tsx`, + content: `import React from 'react' +import { createRoot } from 'react-dom/client' + +const App = () =>

${name}

+ +const root = createRoot(document.getElementById('root')!) +root.render() +`, + }, + packageJson: { + path: `${name}${rawAppSuffix}/package.json`, + content: `{ + "dependencies": { + "react": "19.0.0", + "react-dom": "19.0.0" + } +}`, + }, + inlineScript: { + path: `${name}${rawAppSuffix}/inline_scripts/a.inline_script.ts`, + content: `export async function main(x: string) { + return x +} +`, + }, + inlineScriptLock: { + path: `${name}${rawAppSuffix}/inline_scripts/a.inline_script.lock`, + content: ``, + }, + }; +} + +// ============================================================================= +// Local Creation Functions (Fixture + Write to Disk) +// ============================================================================= + +/** + * Creates a script on the local filesystem. + * See file header for cross-links to related helpers. + * + * This is a convenience function that creates a script fixture and writes it to disk. + * + * @param tempDir - Base directory for the test workspace + * @param path - Relative path within the workspace (e.g., "f/test") + * @param name - Script name (without extension) + * @param language - Script language (default: "bun") + * @param content - Optional custom script content + * + * @example + * await createLocalScript(tempDir, "f/test", "my_script"); + * await createLocalScript(tempDir, "f/test", "custom", "python3", "def main(): return 42"); + * + * @keywords create local script, local script, write script, script on disk + */ +export async function createLocalScript( + tempDir: string, + path: string, + name: string, + language: "python3" | "deno" | "bun" | "bash" | "go" | "postgresql" = "bun", + content?: string +): Promise { + const fixture = createScriptFixture(name, language, content); + await mkdir(`${tempDir}/${path}`, { recursive: true }); + await writeFile( + `${tempDir}/${path}/${fixture.contentFile.path}`, + fixture.contentFile.content, + "utf-8" + ); + await writeFile( + `${tempDir}/${path}/${fixture.metadataFile.path}`, + fixture.metadataFile.content, + "utf-8" + ); +} + +/** + * Creates a flow on the local filesystem. + * See file header for cross-links to related helpers. + * + * This is a convenience function that creates a flow fixture and writes it to disk. + * + * @param tempDir - Base directory for the test workspace + * @param path - Relative path within the workspace (e.g., "f/test") + * @param name - Flow name + * @param inlineScriptContent - Optional custom inline script content + * + * @example + * await createLocalFlow(tempDir, "f/test", "my_flow"); + * + * @keywords create local flow, local flow, write flow, flow on disk + */ +export async function createLocalFlow( + tempDir: string, + path: string, + name: string, + inlineScriptContent?: string +): Promise { + const fixture = createFlowFixture(name, inlineScriptContent); + const flowDir = `${tempDir}/${path}/${name}${getFolderSuffix("flow")}`; + await mkdir(flowDir, { recursive: true }); + + for (const file of Object.values(fixture)) { + const fullPath = `${tempDir}/${path}/${file.path}`; + await writeFile(fullPath, file.content, "utf-8"); + } +} + +/** + * Creates an app on the local filesystem. + * See file header for cross-links to related helpers. + * + * This is a convenience function that creates an app fixture and writes it to disk. + * + * @param tempDir - Base directory for the test workspace + * @param path - Relative path within the workspace (e.g., "f/test") + * @param name - App name + * + * @example + * await createLocalApp(tempDir, "f/test", "my_app"); + * + * @keywords create local app, local app, write app, app on disk + */ +export async function createLocalApp( + tempDir: string, + path: string, + name: string +): Promise { + const fixture = createAppFixture(name); + const appDir = `${tempDir}/${path}/${name}${getFolderSuffix("app")}`; + await mkdir(appDir, { recursive: true }); + + for (const file of Object.values(fixture)) { + const fullPath = `${tempDir}/${path}/${file.path}`; + await writeFile(fullPath, file.content, "utf-8"); + } +} + +/** + * Creates a raw app on the local filesystem. + * See file header for cross-links to related helpers. + * + * Raw apps are React/frontend apps with separate inline scripts. + * This is a convenience function that creates a raw app fixture and writes it to disk. + * + * @param tempDir - Base directory for the test workspace + * @param path - Relative path within the workspace (e.g., "f/test") + * @param name - Raw app name + * + * @example + * await createLocalRawApp(tempDir, "f/test", "my_raw_app"); + * + * @keywords create local raw app, local raw app, write raw app, raw app on disk, react app + */ +export async function createLocalRawApp( + tempDir: string, + path: string, + name: string +): Promise { + const fixture = createRawAppFixture(name); + const rawAppSuffix = getFolderSuffix("raw_app"); + const appDir = `${tempDir}/${path}/${name}${rawAppSuffix}`; + await mkdir(`${appDir}/inline_scripts`, { recursive: true }); + + for (const file of Object.values(fixture)) { + const fullPath = `${tempDir}/${path}/${file.path}`; + const dir = fullPath.substring(0, fullPath.lastIndexOf("/")); + await mkdir(dir, { recursive: true }); + await writeFile(fullPath, file.content, "utf-8"); + } +} + +// ============================================================================= +// Resource Fixtures (Variables, Resources, Schedules, etc.) +// ============================================================================= + +/** + * Creates a resource fixture. + * + * @keywords resource fixture, create resource + */ +export function createResourceFixture( + name: string, + resourceType: string, + value: Record +): FileFixture { + return { + path: `${name}.resource.yaml`, + content: `resource_type: "${resourceType}" +value: +${Object.entries(value) + .map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`) + .join("\n")} +`, + }; +} + +/** + * Creates a variable fixture. + * + * @keywords variable fixture, create variable + */ +export function createVariableFixture( + name: string, + value: string, + isSecret: boolean = false +): FileFixture { + return { + path: `${name}.variable.yaml`, + content: `value: "${value}" +is_secret: ${isSecret} +description: "Variable ${name} for testing" +`, + }; +} + +/** + * Creates a schedule fixture. + * + * @keywords schedule fixture, create schedule + */ +export function createScheduleFixture( + name: string, + scriptPath: string, + schedule: string = "0 * * * *" +): FileFixture { + return { + path: `${name}.schedule.yaml`, + content: `path: "${name}" +schedule: "${schedule}" +script_path: "${scriptPath}" +is_flow: false +args: {} +enabled: true +timezone: "UTC" +`, + }; +} diff --git a/cli/test/unified_generate_metadata.test.ts b/cli/test/unified_generate_metadata.test.ts new file mode 100644 index 0000000000..5cd240dce9 --- /dev/null +++ b/cli/test/unified_generate_metadata.test.ts @@ -0,0 +1,451 @@ +/** + * Unified generate-metadata Command Tests + * + * Tests the new unified `generate-metadata` command that processes + * scripts, flows, and apps together. + */ + +import { expect, test, describe } from "bun:test"; +import { withTestBackend } from "./test_backend.ts"; +import { addWorkspace } from "../workspace.ts"; +import { writeFile } from "node:fs/promises"; +import { + createLocalScript, + createLocalFlow, + createLocalApp, + createLocalRawApp, +} from "./test_fixtures.ts"; + +/** + * Helper to set up a workspace with wmill.yaml + */ +async function setupWorkspace(backend: any, tempDir: string, workspaceName: string) { + const testWorkspace = { + remote: backend.baseUrl, + workspaceId: backend.workspace, + name: workspaceName, + token: backend.token + }; + await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); + + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun +includes: + - "**" +excludes: []`, "utf-8"); +} + +// ============================================================================= +// Main test: processes scripts, flows, and apps together +// ============================================================================= + +test("generate-metadata: processes scripts, flows, and apps together", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "unified_all_test"); + + // Create one of each type + await createLocalScript(tempDir, "f/test", "my_script"); + await createLocalFlow(tempDir, "f/test", "my_flow"); + await createLocalApp(tempDir, "f/test", "my_app"); + + const result = await backend.runCLICommand( + ["generate-metadata", "--yes"], + tempDir, + "unified_all_test" + ); + + expect(result.code).toEqual(0); + // Should find stale items + expect(result.stdout).toContain("Found"); + expect(result.stdout).toContain("stale metadata"); + }); +}); + +// ============================================================================= +// Flag tests +// ============================================================================= + +describe("generate-metadata flags", () => { + test("--includes filters to specific paths", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "includes_test"); + + // Create two scripts in different folders + await createLocalScript(tempDir, "f/included", "script_a"); + await createLocalScript(tempDir, "f/excluded", "script_b"); + + // Run with --includes to only process f/included + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "-i", "f/included/**"], + tempDir, + "includes_test" + ); + + expect(result.code).toEqual(0); + // Should only mention the included script + const output = result.stdout + result.stderr; + expect(output).toContain("script_a"); + expect(output).not.toContain("script_b"); + }); + }); + + test("--excludes filters out specific paths", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "excludes_test"); + + // Create two scripts + await createLocalScript(tempDir, "f/keep", "script_keep"); + await createLocalScript(tempDir, "f/skip", "script_skip"); + + // Run with --excludes to skip f/skip + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "-e", "f/skip/**"], + tempDir, + "excludes_test" + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + expect(output).toContain("script_keep"); + expect(output).not.toContain("script_skip"); + }); + }); + + test("--dry-run shows stale items without updating", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "dry_run_test"); + + await createLocalScript(tempDir, "f/test", "my_script"); + + // Run with --dry-run + const result = await backend.runCLICommand( + ["generate-metadata", "--dry-run"], + tempDir, + "dry_run_test" + ); + + expect(result.code).toEqual(0); + // Should show stale items (Scripts section header) + expect(result.stdout).toContain("Scripts"); + expect(result.stdout).toContain("my_script"); + // Should NOT show "Done" (didn't actually update) + expect(result.stdout).not.toContain("Done"); + + // Run again without --dry-run to verify it would still be stale + const result2 = await backend.runCLICommand( + ["generate-metadata", "--dry-run"], + tempDir, + "dry_run_test" + ); + expect(result2.stdout).toContain("Scripts"); + }); + }); + + test("--lock-only only regenerates locks", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "lock_only_test"); + + await createLocalScript(tempDir, "f/test", "my_script"); + + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "--lock-only"], + tempDir, + "lock_only_test" + ); + + expect(result.code).toEqual(0); + }); + }); + + test("--schema-only only processes scripts (skips flows and apps)", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "schema_only_test"); + + // Create one of each type + await createLocalScript(tempDir, "f/test", "my_script"); + await createLocalFlow(tempDir, "f/test", "my_flow"); + await createLocalApp(tempDir, "f/test", "my_app"); + + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "--schema-only"], + tempDir, + "schema_only_test" + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + // Should show "Checking scripts..." only + expect(output).toContain("Checking scripts..."); + // Should find the script (Scripts section header) + expect(output).toContain("Scripts"); + // Should NOT find flows or apps + expect(output).not.toContain("Flows"); + expect(output).not.toContain("Apps"); + }); + }); + + test("--skip-scripts skips scripts", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "skip_scripts_test"); + + await createLocalScript(tempDir, "f/test", "my_script"); + await createLocalFlow(tempDir, "f/test", "my_flow"); + + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "--skip-scripts"], + tempDir, + "skip_scripts_test" + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + // Should NOT contain script + expect(output).not.toContain("Scripts"); + // Should contain flow + expect(output).toContain("Flows"); + }); + }); + + test("--skip-flows skips flows", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "skip_flows_test"); + + await createLocalScript(tempDir, "f/test", "my_script"); + await createLocalFlow(tempDir, "f/test", "my_flow"); + + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "--skip-flows"], + tempDir, + "skip_flows_test" + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + // Should contain script + expect(output).toContain("Scripts"); + // Should NOT contain flow + expect(output).not.toContain("Flows"); + }); + }); + + test("--skip-apps skips apps", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "skip_apps_test"); + + await createLocalScript(tempDir, "f/test", "my_script"); + await createLocalApp(tempDir, "f/test", "my_app"); + + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "--skip-apps"], + tempDir, + "skip_apps_test" + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + // Should contain script + expect(output).toContain("Scripts"); + // Should NOT contain app + expect(output).not.toContain("Apps"); + }); + }); + + test("shows 'All metadata up-to-date' when nothing to update", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "uptodate_test"); + + // Create a script and run generate-metadata twice + await createLocalScript(tempDir, "f/test", "my_script"); + + // First run - generates metadata + await backend.runCLICommand( + ["generate-metadata", "--yes"], + tempDir, + "uptodate_test" + ); + + // Second run - should be up-to-date + const result = await backend.runCLICommand( + ["generate-metadata", "--yes"], + tempDir, + "uptodate_test" + ); + + expect(result.code).toEqual(0); + expect(result.stdout).toContain("up-to-date"); + }); + }); + + test("skipping all types shows warning", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "skip_all_test"); + + await createLocalScript(tempDir, "f/test", "my_script"); + + const result = await backend.runCLICommand( + ["generate-metadata", "--skip-scripts", "--skip-flows", "--skip-apps"], + tempDir, + "skip_all_test" + ); + + expect(result.code).toEqual(0); + expect(result.stdout).toContain("Nothing to check"); + }); + }); +}); + +// ============================================================================= +// Folder argument tests +// ============================================================================= + +describe("generate-metadata folder argument", () => { + test("filters to specific script folder", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "folder_script_test"); + + // Create scripts in different folders + await createLocalScript(tempDir, "f/included", "script_a"); + await createLocalScript(tempDir, "f/excluded", "script_b"); + + // Run with folder argument + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "f/included/script_a.ts"], + tempDir, + "folder_script_test" + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + expect(output).toContain("script_a"); + expect(output).not.toContain("script_b"); + }); + }); + + test("filters to specific flow folder", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "folder_flow_test"); + + // Create flows in different folders + await createLocalFlow(tempDir, "f/included", "flow_a"); + await createLocalFlow(tempDir, "f/excluded", "flow_b"); + + // Run with folder argument (flow folder path - uses .flow suffix by default) + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "f/included/flow_a.flow"], + tempDir, + "folder_flow_test" + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + expect(output).toContain("flow_a"); + expect(output).not.toContain("flow_b"); + }); + }); + + test("filters to specific app folder", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "folder_app_test"); + + // Create apps in different folders + await createLocalApp(tempDir, "f/included", "app_a"); + await createLocalApp(tempDir, "f/excluded", "app_b"); + + // Run with folder argument (app folder path - uses .app suffix by default) + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "f/included/app_a.app"], + tempDir, + "folder_app_test" + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + expect(output).toContain("app_a"); + expect(output).not.toContain("app_b"); + }); + }); + + test("shows up-to-date when folder has no stale items", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "folder_uptodate_test"); + + await createLocalScript(tempDir, "f/test", "my_script"); + + // First run to generate metadata + await backend.runCLICommand( + ["generate-metadata", "--yes"], + tempDir, + "folder_uptodate_test" + ); + + // Second run with folder - should be up-to-date + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "f/test/my_script.ts"], + tempDir, + "folder_uptodate_test" + ); + + expect(result.code).toEqual(0); + expect(result.stdout).toContain("up-to-date"); + }); + }); + + test("trailing slash is stripped (matches deprecated behavior)", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "trailing_slash_test"); + + await createLocalScript(tempDir, "f/test", "my_script"); + + // Run with trailing slash + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "f/test/my_script.ts/"], + tempDir, + "trailing_slash_test" + ); + + expect(result.code).toEqual(0); + expect(result.stdout).toContain("my_script"); + }); + }); + + test("parent folder matches all children", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "parent_folder_test"); + + // Create scripts in nested folders + await createLocalScript(tempDir, "f/parent", "script_a"); + await createLocalScript(tempDir, "f/parent/child", "script_b"); + await createLocalScript(tempDir, "f/other", "script_c"); + + // Run with parent folder - should match both scripts in f/parent tree + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "f/parent"], + tempDir, + "parent_folder_test" + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + expect(output).toContain("script_a"); + expect(output).toContain("script_b"); + expect(output).not.toContain("script_c"); + }); + }); + + test("non-existent folder shows up-to-date", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "nonexistent_folder_test"); + + await createLocalScript(tempDir, "f/exists", "my_script"); + + // Run with non-existent folder + const result = await backend.runCLICommand( + ["generate-metadata", "--yes", "f/does_not_exist"], + tempDir, + "nonexistent_folder_test" + ); + + expect(result.code).toEqual(0); + expect(result.stdout).toContain("up-to-date"); + }); + }); +}); From 2e430c4c0b8540df7b6997434a7a9f9134858026 Mon Sep 17 00:00:00 2001 From: Alexander Petric Date: Fri, 13 Mar 2026 04:21:15 -0400 Subject: [PATCH 19/25] feat: add GitHub Enterprise Server (GHES) support for GitHub App git sync (#8344) * feat: add GitHub Enterprise Server (GHES) support for GitHub App git sync Add a self-managed GitHub App mode alongside the existing managed (stats.windmill.dev) mode, enabling git sync for GitHub Enterprise Server and custom GitHub App installations. Backend: - Parameterize GitHub API URLs (no more hardcoded github.com) - Add GITHUB_ENTERPRISE_APP_SETTING global setting - Add OpenAPI specs for ghes_installation_callback and ghes_config endpoints Frontend: - Add instance settings UI for configuring self-managed GitHub Apps with setup instructions and validation - GHES installation flow in gh_success page - Dynamic installation URL based on GHES config - Increase git sync test connection timeout to 10s - Block "Review changes" save when settings are invalid EE companion PR: windmill-labs/windmill-ee-private# Co-Authored-By: Claude Opus 4.6 * chore: update ee-repo-ref to c74c86b78a66b976fd9968b21f77903723e668ec This commit updates the EE repository reference after PR #459 was merged in windmill-ee-private. Previous ee-repo-ref: 45e4550110799525b5502cf072c8af8132492638 New ee-repo-ref: c74c86b78a66b976fd9968b21f77903723e668ec Automated by sync-ee-ref workflow. * sqlx --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: windmill-internal-app[bot] Co-authored-by: Ruben Fiszel --- ...096bd0777da52c05e23262572089e0c3c6c46.json | 16 ++ ...b5eea1780226698e419ac59ba55012296913d.json | 28 +++ ...d531b05eaa17fccc599306eb1a96a65ee761.json} | 10 +- ...e98369e1d93514e1618723a34bf96080d4ebc.json | 16 -- ...95c7cda0cba0b119d4fdb8a0dea1b28a1d560.json | 22 --- backend/ee-repo-ref.txt | 2 +- backend/windmill-api/openapi.yaml | 52 ++++++ backend/windmill-common/src/git_sync_oss.rs | 11 +- .../windmill-common/src/global_settings.rs | 1 + .../src/lib/components/InstanceSetting.svelte | 3 + .../lib/components/InstanceSettings.svelte | 19 +- .../lib/components/SuperadminSettings.svelte | 4 +- .../components/SuperadminSettingsInner.svelte | 4 +- .../git_sync/GitSyncContext.svelte.ts | 4 +- .../src/lib/components/instanceSettings.ts | 31 +++- .../instanceSettings/GhesAppSettings.svelte | 172 ++++++++++++++++++ frontend/src/lib/githubApp.ts | 20 +- frontend/src/routes/gh_success/+page.svelte | 70 ++++++- 18 files changed, 426 insertions(+), 59 deletions(-) create mode 100644 backend/.sqlx/query-48b394bd9ca63d33a7ea97113b0096bd0777da52c05e23262572089e0c3c6c46.json create mode 100644 backend/.sqlx/query-ad5fc9212a123a8328397496ef3b5eea1780226698e419ac59ba55012296913d.json rename backend/.sqlx/{query-0ee14619dd81df460b2b8cc6df2b89646279f77469c35deffca8e17a11d7f6c8.json => query-ae7adc583cdd3f876164ed60569ed531b05eaa17fccc599306eb1a96a65ee761.json} (55%) delete mode 100644 backend/.sqlx/query-b4d48c820bf41619bffa8f62367e98369e1d93514e1618723a34bf96080d4ebc.json delete mode 100644 backend/.sqlx/query-be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560.json create mode 100644 frontend/src/lib/components/instanceSettings/GhesAppSettings.svelte diff --git a/backend/.sqlx/query-48b394bd9ca63d33a7ea97113b0096bd0777da52c05e23262572089e0c3c6c46.json b/backend/.sqlx/query-48b394bd9ca63d33a7ea97113b0096bd0777da52c05e23262572089e0c3c6c46.json new file mode 100644 index 0000000000..18a4671616 --- /dev/null +++ b/backend/.sqlx/query-48b394bd9ca63d33a7ea97113b0096bd0777da52c05e23262572089e0c3c6c46.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE workspace_settings\n SET git_app_installations = (\n SELECT jsonb_agg(\n CASE\n WHEN (elem->>'installation_id')::bigint = $2 THEN $1::jsonb\n ELSE elem\n END\n )\n FROM jsonb_array_elements(git_app_installations) AS elem\n )\n WHERE workspace_id = $3\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Jsonb", + "Int8", + "Text" + ] + }, + "nullable": [] + }, + "hash": "48b394bd9ca63d33a7ea97113b0096bd0777da52c05e23262572089e0c3c6c46" +} diff --git a/backend/.sqlx/query-ad5fc9212a123a8328397496ef3b5eea1780226698e419ac59ba55012296913d.json b/backend/.sqlx/query-ad5fc9212a123a8328397496ef3b5eea1780226698e419ac59ba55012296913d.json new file mode 100644 index 0000000000..33ed0b518b --- /dev/null +++ b/backend/.sqlx/query-ad5fc9212a123a8328397496ef3b5eea1780226698e419ac59ba55012296913d.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id,\n elem->>'github_base_url' as github_base_url\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "installation_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "github_base_url", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null, + null + ] + }, + "hash": "ad5fc9212a123a8328397496ef3b5eea1780226698e419ac59ba55012296913d" +} diff --git a/backend/.sqlx/query-0ee14619dd81df460b2b8cc6df2b89646279f77469c35deffca8e17a11d7f6c8.json b/backend/.sqlx/query-ae7adc583cdd3f876164ed60569ed531b05eaa17fccc599306eb1a96a65ee761.json similarity index 55% rename from backend/.sqlx/query-0ee14619dd81df460b2b8cc6df2b89646279f77469c35deffca8e17a11d7f6c8.json rename to backend/.sqlx/query-ae7adc583cdd3f876164ed60569ed531b05eaa17fccc599306eb1a96a65ee761.json index ac882aec64..4bb689c519 100644 --- a/backend/.sqlx/query-0ee14619dd81df460b2b8cc6df2b89646279f77469c35deffca8e17a11d7f6c8.json +++ b/backend/.sqlx/query-ae7adc583cdd3f876164ed60569ed531b05eaa17fccc599306eb1a96a65ee761.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id,\n elem->>'account_id' as account_id\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ", + "query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id,\n elem->>'account_id' as account_id,\n elem->>'github_base_url' as github_base_url\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ", "describe": { "columns": [ { @@ -12,6 +12,11 @@ "ordinal": 1, "name": "account_id", "type_info": "Text" + }, + { + "ordinal": 2, + "name": "github_base_url", + "type_info": "Text" } ], "parameters": { @@ -20,9 +25,10 @@ ] }, "nullable": [ + null, null, null ] }, - "hash": "0ee14619dd81df460b2b8cc6df2b89646279f77469c35deffca8e17a11d7f6c8" + "hash": "ae7adc583cdd3f876164ed60569ed531b05eaa17fccc599306eb1a96a65ee761" } diff --git a/backend/.sqlx/query-b4d48c820bf41619bffa8f62367e98369e1d93514e1618723a34bf96080d4ebc.json b/backend/.sqlx/query-b4d48c820bf41619bffa8f62367e98369e1d93514e1618723a34bf96080d4ebc.json deleted file mode 100644 index 0efca7e867..0000000000 --- a/backend/.sqlx/query-b4d48c820bf41619bffa8f62367e98369e1d93514e1618723a34bf96080d4ebc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE workspace_settings\n SET git_app_installations = (\n SELECT jsonb_agg(\n CASE\n WHEN (elem->>'installation_id')::bigint = $2 THEN $1::jsonb\n ELSE elem\n END\n )\n FROM jsonb_array_elements(git_app_installations) AS elem\n )\n WHERE workspace_id = $3\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Jsonb", - "Int8", - "Text" - ] - }, - "nullable": [] - }, - "hash": "b4d48c820bf41619bffa8f62367e98369e1d93514e1618723a34bf96080d4ebc" -} diff --git a/backend/.sqlx/query-be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560.json b/backend/.sqlx/query-be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560.json deleted file mode 100644 index 182fa4e788..0000000000 --- a/backend/.sqlx/query-be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "installation_id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560" -} diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index ee34a47736..87f42ac45b 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -344e2aa60d8cafe08b8c57445d2f9555bad7625a +c74c86b78a66b976fd9968b21f77903723e668ec diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 3eb3f7f2cf..ad494ee0e1 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1939,6 +1939,58 @@ paths: "200": description: Successfully imported the installation + /w/{workspace}/github_app/ghes_installation_callback: + post: + summary: GHES installation callback + description: Register a self-managed GitHub App installation from GitHub Enterprise Server + operationId: ghesInstallationCallback + tags: + - Git Sync + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - installation_id + properties: + installation_id: + type: integer + format: int64 + description: The GitHub App installation ID from GHES + responses: + "200": + description: GHES installation registered successfully + + /github_app/ghes_config: + get: + summary: Get GHES app config + description: Returns the GitHub Enterprise Server app configuration (without private key) for constructing the installation URL + operationId: getGhesConfig + tags: + - Git Sync + responses: + "200": + description: GHES app configuration + content: + application/json: + schema: + type: object + properties: + base_url: + type: string + app_slug: + type: string + client_id: + type: string + required: + - base_url + - app_slug + - client_id + /users/accept_invite: post: summary: accept invite to workspace diff --git a/backend/windmill-common/src/git_sync_oss.rs b/backend/windmill-common/src/git_sync_oss.rs index a350b74156..4cd7a7c3d1 100644 --- a/backend/windmill-common/src/git_sync_oss.rs +++ b/backend/windmill-common/src/git_sync_oss.rs @@ -21,15 +21,14 @@ pub fn prepend_token_to_github_url( ) -> crate::error::Result { let url = Url::parse(github_url)?; - if url.host_str() != Some("github.com") { - return Err(crate::error::Error::BadRequest( - "Invalid: not a github URL".to_string(), - )); - } + let host = url.host_str().ok_or_else(|| { + crate::error::Error::BadRequest("Invalid GitHub URL: no host".to_string()) + })?; Ok(format!( - "https://x-access-token:{}@github.com{}", + "https://x-access-token:{}@{}{}", installation_token, + host, url.path() )) } diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index 6d2f225479..1d2aeafce8 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -58,6 +58,7 @@ pub const OTEL_TRACING_PROXY_SETTING: &str = "otel_tracing_proxy"; pub const APP_WORKSPACED_ROUTE_SETTING: &str = "app_workspaced_route"; pub const SECRET_BACKEND_SETTING: &str = "secret_backend"; pub const MIN_KEEP_ALIVE_VERSION_SETTING: &str = "min_keep_alive_version"; +pub const GITHUB_ENTERPRISE_APP_SETTING: &str = "github_enterprise_app"; pub const ENV_SETTINGS: &[&str] = &[ "DISABLE_NSJAIL", diff --git a/frontend/src/lib/components/InstanceSetting.svelte b/frontend/src/lib/components/InstanceSetting.svelte index 663bb8aa75..1c69ac8a80 100644 --- a/frontend/src/lib/components/InstanceSetting.svelte +++ b/frontend/src/lib/components/InstanceSetting.svelte @@ -22,6 +22,7 @@ import CriticalAlertChannels from './instanceSettings/CriticalAlertChannels.svelte' import SmtpSettings from './instanceSettings/SmtpSettings.svelte' import SecretBackendConfig from './instanceSettings/SecretBackendConfig.svelte' + import GhesAppSettings from './instanceSettings/GhesAppSettings.svelte' import IndexerMemorySettings from './instanceSettings/IndexerMemorySettings.svelte' import IndexerJobIndexSettings from './instanceSettings/IndexerJobIndexSettings.svelte' import IndexerLogIndexSettings from './instanceSettings/IndexerLogIndexSettings.svelte' @@ -716,6 +717,8 @@ {:else if setting.fieldType == 'secret_backend'} + {:else if setting.fieldType == 'github_enterprise_app'} + {/if} {#if hasError} diff --git a/frontend/src/lib/components/InstanceSettings.svelte b/frontend/src/lib/components/InstanceSettings.svelte index b9d355f323..5f5ac925f4 100644 --- a/frontend/src/lib/components/InstanceSettings.svelte +++ b/frontend/src/lib/components/InstanceSettings.svelte @@ -30,6 +30,7 @@ quickSetup?: boolean yamlMode?: boolean hasUnsavedChanges?: boolean + hasAnyInvalid?: boolean } let { @@ -40,7 +41,8 @@ onNavigateToTab, quickSetup = false, yamlMode = $bindable(false), - hasUnsavedChanges = $bindable(false) + hasUnsavedChanges = $bindable(false), + hasAnyInvalid = $bindable(false) }: Props = $props() let values: Writable> = writable({}) @@ -77,7 +79,8 @@ smtp_settings: {}, otel: {}, indexer_settings: {}, - critical_error_channels: [] + critical_error_channels: [], + github_enterprise_app: {} } function applyFormDefaults(vals: Record): void { @@ -438,6 +441,10 @@ return result }) + $effect(() => { + hasAnyInvalid = Object.values(invalidCategories).some(Boolean) + }) + export function isDirty(category: string): boolean { return dirtyCategories[category] ?? false } @@ -601,7 +608,8 @@ secret_backend: ['token'], object_store_cache_config: ['secret_key', 'serviceAccountKey'], custom_instance_pg_databases: ['user_pwd'], - rsa_keys: ['private_key'] + rsa_keys: ['private_key'], + github_enterprise_app: ['private_key'] } /** Returns SENSITIVE_UNCHANGED if the value is non-empty and matches the initial */ @@ -1008,6 +1016,11 @@ description="Configure where secrets (secret variables) are stored." link="https://www.windmill.dev/docs/core_concepts/workspace_secret_encryption" /> + {:else if category == 'GitHub Enterprise App'} + {:else if category == 'Auth/OAuth/SAML'} @@ -159,7 +161,7 @@ options={{ right: 'Unified' }} size="xs" /> - + {/snippet}
diff --git a/frontend/src/lib/components/SuperadminSettingsInner.svelte b/frontend/src/lib/components/SuperadminSettingsInner.svelte index 1f33e768d8..37fca0037b 100644 --- a/frontend/src/lib/components/SuperadminSettingsInner.svelte +++ b/frontend/src/lib/components/SuperadminSettingsInner.svelte @@ -46,7 +46,8 @@ closeDrawer, showHeaderInfo = true, yamlMode = $bindable(false), - hasUnsavedChanges = $bindable(false) + hasUnsavedChanges = $bindable(false), + hasAnyInvalid = $bindable(false) } = $props() function removeHash() { @@ -510,6 +511,7 @@ hideTabs bind:yamlMode bind:hasUnsavedChanges + bind:hasAnyInvalid tab={instanceSettingsCategory} {authSubTab} {closeDrawer} diff --git a/frontend/src/lib/components/git_sync/GitSyncContext.svelte.ts b/frontend/src/lib/components/git_sync/GitSyncContext.svelte.ts index 1940296534..834982c3f3 100644 --- a/frontend/src/lib/components/git_sync/GitSyncContext.svelte.ts +++ b/frontend/src/lib/components/git_sync/GitSyncContext.svelte.ts @@ -564,8 +564,8 @@ export function createGitSyncContext(workspace: string) { // Use JobManager for polling await jobManager.runWithProgress(() => Promise.resolve(jobId), { workspace, - timeout: 5000, - timeoutMessage: 'Git sync test job timed out after 5s', + timeout: 10000, + timeoutMessage: 'Git sync test job timed out after 10s', onProgress: (status) => { gitSyncTestJobs[idx].status = status.status === 'success' diff --git a/frontend/src/lib/components/instanceSettings.ts b/frontend/src/lib/components/instanceSettings.ts index 0c0fd51578..450555680c 100644 --- a/frontend/src/lib/components/instanceSettings.ts +++ b/frontend/src/lib/components/instanceSettings.ts @@ -51,6 +51,7 @@ export interface Setting { | 'otel' | 'otel_tracing_proxy' | 'secret_backend' + | 'github_enterprise_app' storage: SettingStorage advancedToggle?: { label: string @@ -665,6 +666,23 @@ export const settings: Record = { storage: 'setting', ee_only: 'HashiCorp Vault integration is an Enterprise Edition feature' } + ], + 'GitHub Enterprise App': [ + { + label: 'GitHub Enterprise App', + description: + 'Configure a self-managed GitHub App for GitHub Enterprise Server (or any GitHub instance) to enable git sync without stats.windmill.dev.', + key: 'github_enterprise_app', + fieldType: 'github_enterprise_app', + storage: 'setting', + ee_only: '', + error: + 'When self-managed mode is enabled, Base URL, App ID, App Slug, and Private Key are required.', + isValid: (v: any) => { + if (!v?.self_managed) return true + return !!(v?.base_url && v?.app_id && v?.app_slug && v?.private_key) + } + } ] } @@ -772,6 +790,13 @@ export const instanceSettingsNavigationGroups = [ { title: 'Advanced', items: [ + { + id: 'github_enterprise_app', + label: 'GitHub Enterprise App', + aiId: 'instance-settings-github-enterprise-app', + aiDescription: 'Self-managed GitHub App for GitHub Enterprise Server git sync', + isEE: true + }, { id: 'private_hub', label: 'Private Hub', @@ -809,7 +834,8 @@ export const tabToCategoryMap: Record = { secret_storage: 'Secret Storage', object_storage: 'Object Storage', jobs: 'Jobs', - private_hub: 'Private Hub' + private_hub: 'Private Hub', + github_enterprise_app: 'GitHub Enterprise App' } export const tabToAuthSubTab: Record = { @@ -838,7 +864,8 @@ export const categoryToTabMap: Record = { 'Secret Storage': 'secret_storage', 'Object Storage': 'object_storage', Jobs: 'jobs', - 'Private Hub': 'private_hub' + 'Private Hub': 'private_hub', + 'GitHub Enterprise App': 'github_enterprise_app' } export interface SearchableSettingItem { diff --git a/frontend/src/lib/components/instanceSettings/GhesAppSettings.svelte b/frontend/src/lib/components/instanceSettings/GhesAppSettings.svelte new file mode 100644 index 0000000000..6ded7d82f2 --- /dev/null +++ b/frontend/src/lib/components/instanceSettings/GhesAppSettings.svelte @@ -0,0 +1,172 @@ + + +
+ { + $values['github_enterprise_app'] = { + ...$values['github_enterprise_app'], + self_managed: !selfManaged + } + }} + /> + + {#if !selfManaged} +

+ Using the managed Windmill GitHub App via stats.windmill.dev. Enable self-managed mode to + configure your own GitHub App (required for GitHub Enterprise Server). +

+ {:else} +
+ How to create a GitHub App +
+

+ 1. On your GitHub instance, go to + Settings → Developer settings → GitHub Apps → New GitHub App. +

+

2. Fill in the required fields:

+
    +
  • + GitHub App name: e.g. windmill-sync (this becomes the app + slug) +
  • +
  • + Homepage URL: your Windmill instance URL +
  • +
  • + Callback URL: <your-windmill-url>/gh_success +
  • +
  • + Setup URL (optional): + <your-windmill-url>/gh_success with "Redirect on update" checked +
  • +
  • Uncheck Active under Webhook (not needed)
  • +
+

3. Set repository permissions:

+
    +
  • Contents: Read & write
  • +
  • Metadata: Read-only
  • +
+

+ 4. Under "Where can this GitHub App be installed?", choose + Any account (or restrict to your organization). +

+

+ 5. Click Create GitHub App. On the next page, note the + App ID and Client ID. +

+

+ 6. Scroll down and click Generate a private key. Save the + downloaded .pem file — paste its contents into the Private Key field below. +

+

+ 7. The App Slug is the URL-friendly name shown in the + app's URL (e.g. github.com/apps/windmill-sync). +

+

+ 8. The Base URL is your GitHub instance root (e.g. + https://github.com or https://github.mycompany.com). +

+
+
+ {/if} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
diff --git a/frontend/src/lib/githubApp.ts b/frontend/src/lib/githubApp.ts index 74ce0438ed..48c2cd1b56 100644 --- a/frontend/src/lib/githubApp.ts +++ b/frontend/src/lib/githubApp.ts @@ -1,4 +1,8 @@ -import { GitSyncService, type GetGlobalConnectedRepositoriesResponse } from '$lib/gen' +import { + GitSyncService, + type GetGlobalConnectedRepositoriesResponse, + type GetGhesConfigResponse +} from '$lib/gen' import { sendUserToast } from '$lib/toast' import { base } from '$lib/base' @@ -129,7 +133,19 @@ export async function loadGithubInstallations( }) ) - state.githubInstallationUrl = `https://github.com/apps/windmill-sync-helper/installations/new?state=${stateParam}` + // Check if GHES app is configured; if so, use GHES installation URL + try { + const ghesConfig: GetGhesConfigResponse = await GitSyncService.getGhesConfig() + if (ghesConfig?.base_url && ghesConfig?.app_slug) { + const ghesBaseUrl = ghesConfig.base_url.replace(/\/$/, '') + state.githubInstallationUrl = `${ghesBaseUrl}/apps/${ghesConfig.app_slug}/installations/new?state=${stateParam}` + } else { + state.githubInstallationUrl = `https://github.com/apps/windmill-sync-helper/installations/new?state=${stateParam}` + } + } catch { + // No GHES config — use default github.com URL + state.githubInstallationUrl = `https://github.com/apps/windmill-sync-helper/installations/new?state=${stateParam}` + } } catch (err) { const githubError = handleGitHubAppError(err, 'load installations') sendUserToast(`Failed to load GitHub installations: ${githubError.message}`, true) diff --git a/frontend/src/routes/gh_success/+page.svelte b/frontend/src/routes/gh_success/+page.svelte index 3886857126..eab7502757 100644 --- a/frontend/src/routes/gh_success/+page.svelte +++ b/frontend/src/routes/gh_success/+page.svelte @@ -13,6 +13,74 @@ onMount(async () => { const url = new URL(window.location.href) + + // Check for GHES flow: GitHub redirects back with installation_id and state params + // (no jwt_token param — that's the managed flow via stats.windmill.dev) + const jwt_token = url.searchParams.get('jwt_token') || '' + const stateParam = url.searchParams.get('state') || '' + + if (!jwt_token && stateParam) { + // GHES self-managed flow + await handleGhesFlow(url, stateParam) + } else { + // Managed flow (existing) + await handleManagedFlow(url) + } + }) + + async function handleGhesFlow(url: URL, stateParam: string) { + const installation_id_str = url.searchParams.get('installation_id') || '' + const installation_id = parseInt(installation_id_str, 10) + + let workspace_id: string + try { + const state = JSON.parse(decodeURIComponent(stateParam)) + workspace_id = state.workspace_id + } catch { + isLoading = false + errorMessage = 'Invalid state parameter' + sendUserToast('Invalid state parameter in the URL', true) + return + } + + if (!workspace_id || isNaN(installation_id)) { + isLoading = false + errorMessage = 'Missing or invalid required parameters' + sendUserToast('Missing or invalid required parameters in the URL', true) + return + } + + try { + const response = await fetch( + `/api/w/${workspace_id}/github_app/ghes_installation_callback`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + installation_id + }) + } + ) + + if (!response.ok) { + const errorData = await response.text() + throw new Error(errorData || 'Failed to complete GitHub Enterprise app installation') + } + + isSuccess = true + sendUserToast('GitHub Enterprise app installed successfully', false) + } catch (error) { + console.error('Error during GitHub Enterprise app installation:', error) + errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + sendUserToast(`Error installing GitHub Enterprise app: ${errorMessage}`, true) + } finally { + isLoading = false + } + } + + async function handleManagedFlow(url: URL) { const workspace_id = url.searchParams.get('workspace_id') || '' const installation_id_str = url.searchParams.get('installation_id') || '' const account_id = url.searchParams.get('account_id') || '' @@ -54,7 +122,7 @@ } finally { isLoading = false } - }) + }
From 2d5b72b3cea7430a5dd239aff59da17be76fad45 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 13 Mar 2026 08:50:19 +0000 Subject: [PATCH 20/25] chore: update vite/vitest to stable v8/v4 and remove legacy-peer-deps (#8349) Co-authored-by: Claude Opus 4.5 --- frontend/.npmrc | 2 +- frontend/package-lock.json | 1552 ++++++++++++++++++++++++++++++------ frontend/package.json | 4 +- 3 files changed, 1298 insertions(+), 260 deletions(-) diff --git a/frontend/.npmrc b/frontend/.npmrc index 521a9f7c07..8b13789179 100644 --- a/frontend/.npmrc +++ b/frontend/.npmrc @@ -1 +1 @@ -legacy-peer-deps=true + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cae11fc06e..6de3434311 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -147,9 +147,9 @@ "tar": "^7.5.4", "tslib": "^2.6.1", "typescript": "^5.5.0", - "vite": "^8.0.0-beta.16", + "vite": "^8.0.0", "vite-plugin-mkcert": "^1.17.5", - "vitest": "^4.1.0-beta.5", + "vitest": "^4.1.0", "vitest-browser-svelte": "^2.0.1" }, "optionalDependencies": { @@ -256,6 +256,33 @@ "tslib": "^2.3.1" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -736,23 +763,91 @@ "@codingame/monaco-vscode-view-title-bar-service-override": "25.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "node_modules/@csstools/css-parser-algorithms": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", + "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", "license": "MIT", "optional": true, "dependencies": { @@ -760,10 +855,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", "license": "MIT", "optional": true, "dependencies": { @@ -1098,7 +1192,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1109,7 +1202,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1120,7 +1212,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1130,14 +1221,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1258,7 +1347,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1326,7 +1414,7 @@ "version": "0.115.0", "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1336,7 +1424,7 @@ "version": "0.115.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -1389,7 +1477,7 @@ "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@popperjs/core": { @@ -1409,13 +1497,12 @@ "license": "SEE LICENSE IN LICENSE" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.6.tgz", - "integrity": "sha512-kvjTSWGcrv+BaR2vge57rsKiYdVR8V8CoS0vgKrc570qRBfty4bT+1X0z3j2TaVV+kAYzA0PjeB9+mdZyqUZlg==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1426,13 +1513,12 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.6.tgz", - "integrity": "sha512-+tJhD21KvGNtUrpLXrZQlT+j5HZKiEwR2qtcZb3vNOUpvoT9QjEykr75ZW/Kr0W89gose/HVXU6351uVZD8Qvw==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1443,13 +1529,12 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.6.tgz", - "integrity": "sha512-DKNhjMk38FAWaHwUt1dFR3rA/qRAvn2NUvSG2UGvxvlMxSmN/qqww/j4ABAbXhNRXtGQNmrAINMXRuwHl16ZHg==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1460,13 +1545,12 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.6.tgz", - "integrity": "sha512-8TThsRkCPAnfyMBShxrGdtoOE6h36QepqRQI97iFaQSCRbHFWHcDHppcojZnzXoruuhPnjMEygzaykvPVJsMRg==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1477,13 +1561,12 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.6.tgz", - "integrity": "sha512-ZfmFoOwPUZCWtGOVC9/qbQzfc0249FrRUOzV2XabSMUV60Crp211OWLQN1zmQAsRIVWRcEwhJ46Z1mXGo/L/nQ==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1494,13 +1577,12 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.6.tgz", - "integrity": "sha512-ZsGzbNETxPodGlLTYHaCSGVhNN/rvkMDCJYHdT7PZr5jFJRmBfmDi2awhF64Dt2vxrJqY6VeeYSgOzEbHRsb7Q==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1511,13 +1593,44 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.6.tgz", - "integrity": "sha512-elPpdevtCdUOqziemR86C4CSCr/5sUxalzDrf/CJdMT+kZt2C556as++qHikNOz0vuFf52h+GJNXZM08eWgGPQ==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], "license": "MIT", "optional": true, "os": [ @@ -1528,13 +1641,12 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.6.tgz", - "integrity": "sha512-IBwXsf56o3xhzAyaZxdM1CX8UFiBEUFCjiVUgny67Q8vPIqkjzJj0YKhd3TbBHanuxThgBa59f6Pgutg2OGk5A==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1545,13 +1657,12 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.6.tgz", - "integrity": "sha512-vOk7G8V9Zm+8a6PL6JTpCea61q491oYlGtO6CvnsbhNLlKdf0bbCPytFzGQhYmCKZDKkEbmnkcIprTEGCURnwg==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1562,13 +1673,12 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.6.tgz", - "integrity": "sha512-ASjEDI4MRv7XCQb2JVaBzfEYO98JKCGrAgoW6M03fJzH/ilCnC43Mb3ptB9q/lzsaahoJyIBoAGKAYEjUvpyvQ==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1579,13 +1689,12 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.6.tgz", - "integrity": "sha512-mYa1+h2l6Zc0LvmwUh0oXKKYihnw/1WC73vTqw+IgtfEtv47A+rWzzcWwVDkW73+UDr0d/Ie/HRXoaOY22pQDw==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1596,13 +1705,12 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.6.tgz", - "integrity": "sha512-e2ABskbNH3MRUBMjgxaMjYIw11DSwjLJxBII3UgpF6WClGLIh8A20kamc+FKH5vIaFVnYQInmcLYSUVpqMPLow==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1613,13 +1721,12 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.6.tgz", - "integrity": "sha512-dJVc3ifhaRXxIEh1xowLohzFrlQXkJ66LepHm+CmSprTWgVrPa8Fx3OL57xwIqDEH9hufcKkDX2v65rS3NZyRA==", + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1630,10 +1737,10 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.6.tgz", - "integrity": "sha512-Y0+JT8Mi1mmW08K6HieG315XNRu4L0rkfCpA364HtytjgiqYnMYRdFPcxRl+BQQqNXzecL2S9nii+RUpO93XIA==", - "dev": true, + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "devOptional": true, "license": "MIT" }, "node_modules/@rollup/rollup-linux-x64-gnu": { @@ -1682,7 +1789,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@streamparser/json": { @@ -1704,7 +1811,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -1724,7 +1830,7 @@ "version": "2.53.4", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz", "integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -1819,7 +1925,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz", "integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "deepmerge": "^4.3.1", @@ -1926,7 +2032,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1948,7 +2053,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/d3": { @@ -2256,7 +2361,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/geojson": { @@ -2304,12 +2408,28 @@ "@types/unist": "*" } }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -2321,7 +2441,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/unist": { @@ -2657,16 +2776,16 @@ "license": "ISC" }, "node_modules/@vitest/expect": { - "version": "4.1.0-beta.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0-beta.5.tgz", - "integrity": "sha512-rGZIMfkb+iEjL5+ulpWON5NY1y3bEc+I3btLlayKZ8wvsLhpS+wduc6DaNLWhFWxYwFXmnrLdMd9NRUE9cwySw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0-beta.5", - "@vitest/utils": "4.1.0-beta.5", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" }, @@ -2675,13 +2794,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.0-beta.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0-beta.5.tgz", - "integrity": "sha512-oUE5vFOY7onbFjFGSbWSV6ryDbVmymRUQOQf978k6ZA7EmyRykUISqTiVZVzv/dpPH3IhnctJtHdoglYNG+SPQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.0-beta.5", + "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2690,7 +2809,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -2702,9 +2821,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0-beta.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0-beta.5.tgz", - "integrity": "sha512-QH/FGecnl2uwLveL/n1awB/nm/dJL9M0vMKVwmW0tvLAqTOp5GQQOypRuVvpXNFGhIl2bfpUSjruuDQlCBeFjw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "dev": true, "license": "MIT", "dependencies": { @@ -2715,13 +2834,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.0-beta.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0-beta.5.tgz", - "integrity": "sha512-9OP3INBsI9NhX0+n5syXtZOHooSFl+ctkDVbav6KlCO3CBV2g4TUPllYRAHsKWb6FvkpM2BVO8yO5jgqaZqnBA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0-beta.5", + "@vitest/utils": "4.1.0", "pathe": "^2.0.3" }, "funding": { @@ -2736,14 +2855,14 @@ "license": "MIT" }, "node_modules/@vitest/snapshot": { - "version": "4.1.0-beta.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0-beta.5.tgz", - "integrity": "sha512-ny6wFeFmA700AzTuB5qMSTKsLXtCz8m7CI2ESlcuGMrI7d9kdVUVD6ziRt0Hp2M3C3jIWvd5C6f4ZczAEFoytQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0-beta.5", - "@vitest/utils": "4.1.0-beta.5", + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2759,9 +2878,9 @@ "license": "MIT" }, "node_modules/@vitest/spy": { - "version": "4.1.0-beta.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0-beta.5.tgz", - "integrity": "sha512-eeiIMRR/xBXJxzGhbims+4UOim2bVXzSNf9bLRi0iHWWnXXaK/DS0pYpkLVO83EcqUWbBlqMx9R8Y6tLF6aCJQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", "dev": true, "license": "MIT", "funding": { @@ -2769,13 +2888,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.0-beta.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0-beta.5.tgz", - "integrity": "sha512-yDobPgmVL/4YhVXsbBcmeUb5CIdZiJkoonPnuJXKOxmnj0XZyu7OgIX3KLOcRStbia3nJZ9VIIBWoSv+HS+wVA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0-beta.5", + "@vitest/pretty-format": "4.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, @@ -2813,6 +2932,13 @@ "@xterm/xterm": "^5.0.0" } }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT", + "peer": true + }, "node_modules/@xyflow/svelte": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-1.3.1.tgz", @@ -2887,7 +3013,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3066,7 +3191,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -3082,6 +3206,17 @@ "node": ">=8" } }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -3099,6 +3234,17 @@ "node": ">=12" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -3167,7 +3313,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -3183,6 +3328,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3499,6 +3652,68 @@ "node": ">= 6" } }, + "node_modules/camelcase-keys": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", + "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "camelcase": "^6.3.0", + "map-obj": "^4.1.0", + "quick-lru": "^5.1.1", + "type-fest": "^1.2.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -3726,7 +3941,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3843,7 +4057,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -3862,6 +4076,34 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-fetch": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", @@ -3899,6 +4141,17 @@ "postcss": "^8.0.9" } }, + "node_modules/css-functions-list": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.3.3.tgz", + "integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -4241,6 +4494,60 @@ } } }, + "node_modules/decamelize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", + "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -4318,7 +4625,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4420,7 +4727,6 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", - "dev": true, "license": "MIT" }, "node_modules/devlop": { @@ -4716,6 +5022,17 @@ "errno": "cli.js" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -5143,7 +5460,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -5340,6 +5656,17 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastpriorityqueue": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/fastpriorityqueue/-/fastpriorityqueue-0.7.2.tgz", @@ -5710,6 +6037,50 @@ "node": "*" } }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -5747,6 +6118,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5816,6 +6195,17 @@ "uglify-js": "^3.1.4" } }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6027,6 +6417,56 @@ "node": ">=12.0.0" } }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/html-void-elements": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", @@ -6097,6 +6537,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6107,6 +6558,20 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -6129,8 +6594,8 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/inline-style-parser": { "version": "0.1.1", @@ -6165,6 +6630,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -6262,11 +6735,32 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -6346,7 +6840,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -6358,6 +6852,14 @@ "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", "license": "BSD-3-Clause" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -6387,6 +6889,14 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/json-refs": { "version": "3.0.15", "resolved": "https://registry.npmjs.org/json-refs/-/json-refs-3.0.15.tgz", @@ -6546,11 +7056,22 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -6782,10 +7303,10 @@ "license": "MIT" }, "node_modules/lightningcss": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", - "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", - "dev": true, + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "devOptional": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -6798,27 +7319,26 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.31.1", - "lightningcss-darwin-arm64": "1.31.1", - "lightningcss-darwin-x64": "1.31.1", - "lightningcss-freebsd-x64": "1.31.1", - "lightningcss-linux-arm-gnueabihf": "1.31.1", - "lightningcss-linux-arm64-gnu": "1.31.1", - "lightningcss-linux-arm64-musl": "1.31.1", - "lightningcss-linux-x64-gnu": "1.31.1", - "lightningcss-linux-x64-musl": "1.31.1", - "lightningcss-win32-arm64-msvc": "1.31.1", - "lightningcss-win32-x64-msvc": "1.31.1" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", - "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6833,13 +7353,12 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", - "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6854,13 +7373,12 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", - "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6875,13 +7393,12 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6896,13 +7413,12 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", - "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6917,13 +7433,12 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", - "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6938,13 +7453,12 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", - "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6959,13 +7473,12 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", - "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6980,13 +7493,12 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", - "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7001,13 +7513,12 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", - "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7022,13 +7533,12 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", - "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7066,7 +7576,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -7131,6 +7640,14 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -7186,12 +7703,25 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mapbox-to-css-font": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.5.tgz", @@ -7229,6 +7759,18 @@ "node": ">= 0.4" } }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -7458,6 +8000,48 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/meow": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", + "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/minimist": "^1.2.2", + "camelcase-keys": "^7.0.0", + "decamelize": "^5.0.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.2", + "read-pkg-up": "^8.0.0", + "redent": "^4.0.0", + "trim-newlines": "^4.0.2", + "type-fest": "^1.2.2", + "yargs-parser": "^20.2.9" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8138,6 +8722,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -8267,7 +8867,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -8424,6 +9024,23 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -8547,7 +9164,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, + "devOptional": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" @@ -8753,6 +9370,26 @@ "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", "license": "MIT" }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -8908,7 +9545,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { @@ -9020,10 +9657,10 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "devOptional": true, "funding": [ { "type": "opencollective", @@ -9611,6 +10248,14 @@ "postcss": "^8.4.31" } }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/postcss-safe-parser": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", @@ -9727,7 +10372,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -10089,6 +10734,73 @@ "pify": "^2.3.0" } }, + "node_modules/read-pkg": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", + "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^3.0.2", + "parse-json": "^5.2.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz", + "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "find-up": "^5.0.0", + "read-pkg": "^6.0.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -10118,6 +10830,24 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/redent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -10324,14 +11054,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.6.tgz", - "integrity": "sha512-B8vFPV1ADyegoYfhg+E7RAucYKv0xdVlwYYsIJgfPNeiSxZGWNxts9RqhyGzC11ULK/VaeXyKezGCwpMiH8Ktw==", - "dev": true, + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "devOptional": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.115.0", - "@rolldown/pluginutils": "1.0.0-rc.6" + "@rolldown/pluginutils": "1.0.0-rc.9" }, "bin": { "rolldown": "bin/cli.mjs" @@ -10340,19 +11070,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.6", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.6", - "@rolldown/binding-darwin-x64": "1.0.0-rc.6", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.6", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.6", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.6", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.6", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.6", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.6", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.6", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.6", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.6", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.6" + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" } }, "node_modules/run-parallel": { @@ -10469,7 +11201,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz", "integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/set-function-length": { @@ -10680,7 +11412,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@polka/url": "^1.0.0-next.24", @@ -10701,6 +11433,25 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/sort-asc": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz", @@ -10743,7 +11494,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -10759,6 +11510,46 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0", + "peer": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0", + "peer": true + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -10774,9 +11565,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, @@ -10845,6 +11636,20 @@ "node": ">=8" } }, + "node_modules/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10858,6 +11663,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", + "dev": true, + "license": "ISC", + "peer": true + }, "node_modules/style-to-object": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", @@ -10899,6 +11712,66 @@ "node": ">=4" } }, + "node_modules/stylelint": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.11.0.tgz", + "integrity": "sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@csstools/css-parser-algorithms": "^2.3.1", + "@csstools/css-tokenizer": "^2.2.0", + "@csstools/media-query-list-parser": "^2.1.4", + "@csstools/selector-specificity": "^3.0.0", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^8.2.0", + "css-functions-list": "^3.2.1", + "css-tree": "^2.3.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.1", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^7.0.0", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^5.2.4", + "import-lazy": "^4.0.0", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.29.0", + "mathml-tag-names": "^2.1.3", + "meow": "^10.1.5", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.28", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.0.13", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "style-search": "^0.1.0", + "supports-hyperlinks": "^3.0.0", + "svg-tags": "^1.0.0", + "table": "^6.8.1", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + } + }, "node_modules/stylelint-config-recommended": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-13.0.0.tgz", @@ -10912,6 +11785,78 @@ "stylelint": "^15.10.0" } }, + "node_modules/stylelint/node_modules/@csstools/selector-specificity": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz", + "integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peer": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.13" + } + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-7.0.2.tgz", + "integrity": "sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flat-cache": "^3.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/stylelint/node_modules/known-css-properties": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", + "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/stylelint/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stylelint/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -11036,6 +11981,24 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -11053,7 +12016,6 @@ "version": "5.53.5", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.5.tgz", "integrity": "sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -11377,6 +12339,13 @@ "typescript": "^4.9.4 || ^5.0.0" } }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true, + "peer": true + }, "node_modules/svgo": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", @@ -11420,6 +12389,24 @@ "dev": true, "license": "MIT" }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/tailwind-merge": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", @@ -11651,7 +12638,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -11668,7 +12655,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -11686,7 +12673,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -11696,9 +12683,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -11722,7 +12709,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -11744,6 +12731,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/trim-newlines": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", + "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -11843,7 +12844,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12052,6 +13053,18 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -12095,17 +13108,17 @@ } }, "node_modules/vite": { - "version": "8.0.0-beta.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0-beta.16.tgz", - "integrity": "sha512-c0t7hYkxsjws89HH+BUFh/sL3BpPNhNsL9CJrTpMxBmwKQBRSa5OJ5w4o9O0bQVI/H/vx7UpUUIevvXa37NS/Q==", - "dev": true, + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "devOptional": true, "license": "MIT", "dependencies": { "@oxc-project/runtime": "0.115.0", - "lightningcss": "^1.31.1", + "lightningcss": "^1.32.0", "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rolldown": "1.0.0-rc.6", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "bin": { @@ -12195,7 +13208,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -12208,7 +13221,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", - "dev": true, + "devOptional": true, "license": "MIT", "workspaces": [ "tests/deps/*", @@ -12225,26 +13238,26 @@ } }, "node_modules/vitest": { - "version": "4.1.0-beta.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0-beta.5.tgz", - "integrity": "sha512-oFoeAOQednbyV7mR1hAmT4/yQ4xnNzvHUcU0lFwxo8riim0wsuh2EXF/xOtsT3q33ACeufP5BkWFVyJUC5B/DQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0-beta.5", - "@vitest/mocker": "4.1.0-beta.5", - "@vitest/pretty-format": "4.1.0-beta.5", - "@vitest/runner": "4.1.0-beta.5", - "@vitest/snapshot": "4.1.0-beta.5", - "@vitest/spy": "4.1.0-beta.5", - "@vitest/utils": "4.1.0-beta.5", + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", @@ -12265,10 +13278,10 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0-beta.5", - "@vitest/browser-preview": "4.1.0-beta.5", - "@vitest/browser-webdriverio": "4.1.0-beta.5", - "@vitest/ui": "4.1.0-beta.5", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" @@ -12779,6 +13792,21 @@ "devOptional": true, "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/xml-utils": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz", @@ -12960,6 +13988,17 @@ "node": ">=12" } }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yargs/node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -13003,7 +14042,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "dev": true, "license": "MIT" }, "node_modules/zod": { diff --git a/frontend/package.json b/frontend/package.json index 1412bceecc..d34ea12a90 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -70,9 +70,9 @@ "tar": "^7.5.4", "tslib": "^2.6.1", "typescript": "^5.5.0", - "vite": "^8.0.0-beta.16", + "vite": "^8.0.0", "vite-plugin-mkcert": "^1.17.5", - "vitest": "^4.1.0-beta.5", + "vitest": "^4.1.0", "vitest-browser-svelte": "^2.0.1" }, "overrides": { From 96229575e6641b9c294cf45ed0d9c530a94ffca9 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Fri, 13 Mar 2026 13:07:49 +0100 Subject: [PATCH 21/25] =?UTF-8?q?chore:=20dev=20tooling=20=E2=80=94=20wm-t?= =?UTF-8?q?s-nav=20navigator,=20format=20hooks,=20review=20skill=20(#8337)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: remove wm-cursor, add local-review skill, update PR skill for EE - Remove the unused wm-cursor script and all references to it in README_WORKMUX_DEV.md and worktree-common.sh - Add /local-review skill for code review (bugs + CLAUDE.md compliance) - Add EE companion PR workflow to the /pr skill Co-Authored-By: Claude Opus 4.6 * chore: add wm-ts-nav tree-sitter navigator and fix format hooks - Add wm-ts-nav: standalone tree-sitter code navigator with SQLite index for fast symbol search, definition lookup, and file outlines across Rust, TypeScript, and Svelte files (~12ms warm, ~1s cold for 482 files) - Fix format hooks: surface errors instead of swallowing with 2>/dev/null, use direct prettier path with svelte plugin, add success feedback - Add wm-ts-nav commands to settings allow list - Document wm-ts-nav usage in CLAUDE.md Co-Authored-By: Claude Opus 4.6 * feat(wm-ts-nav): add refs command and --parent filter - refs: find usages of a symbol in code, skipping comments and strings (tree-sitter AST walk, ~46ms for 482 files vs grep's 4ms but no noise) - --parent filter on search: find all methods on a type across all files (e.g. search "%" --kind function --parent ServiceName) - Update CLAUDE.md with clearer when-to-use guidance Co-Authored-By: Claude Opus 4.6 * feat(wm-ts-nav): index refs in DB with import-path resolution Co-Authored-By: Claude Opus 4.5 * feat(wm-ts-nav): add body, callers, callees commands and refs --file/--caller - body: extract a symbol's source code from disk using indexed line ranges - callers: cross-file call graph via SQL join of refs + symbols tables - callees: list all identifiers referenced within a symbol's body - refs --file: scope results to files matching a substring - refs --caller: annotate each ref with the containing function name Co-Authored-By: Claude Opus 4.6 * feat(wm-ts-nav): add auto-rebuilding wrapper script The `wm-ts-nav/nav` wrapper checks if source files are newer than the binary and rebuilds automatically. Invoked via `sh wm-ts-nav/nav` to avoid needing executable permissions after clone. Co-Authored-By: Claude Opus 4.6 * docs: tighten CLAUDE.md nav section for actionable guidance Remove redundant question→command mapping, latency numbers, and excessive examples. Lead with "prefer wm-ts-nav over Read to save context window" and keep only the patterns that change behavior. Co-Authored-By: Claude Opus 4.6 * chore: revert backend/Cargo.lock to main Co-Authored-By: Claude Opus 4.6 * chore: promote wm-ts-nav in workflow, copy binary to worktrees - CLAUDE.md: integrate wm-ts-nav into Workflow step 1 and Core Principles so agents use outline/body before full file reads - workmux: copy built binary via files.copy - worktree-common.sh: copy binary in wm_copy_dependencies for webmux Co-Authored-By: Claude Opus 4.6 * fix(wm-ts-nav): fix double indexing, add TSX grammar, remove needless clone - Reuse index stats from the pre-query update instead of indexing twice on the Index command - Add Lang::Tsx variant so .tsx/.jsx files use LANGUAGE_TSX instead of LANGUAGE_TYPESCRIPT (Svelte stays on TS since script blocks are pure TS) - Remove source.clone() for non-Svelte files — move directly instead Co-Authored-By: Claude Opus 4.6 * fix(wm-ts-nav): fix svelte line numbers, add class methods, innermost caller Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.6 --- .claude/hooks/format-backend.sh | 6 +- .claude/hooks/format-frontend.sh | 6 +- .claude/settings.json | 6 + .claude/skills/local-review/SKILL.md | 98 ++++ .claude/skills/pr/SKILL.md | 23 + .workmux.yaml | 1 + CLAUDE.md | 38 +- README_WORKMUX_DEV.md | 78 --- docs/enterprise.md | 15 +- scripts/wm-cursor | 401 --------------- scripts/worktree-common.sh | 8 +- wm-ts-nav/Cargo.lock | 620 +++++++++++++++++++++++ wm-ts-nav/Cargo.toml | 21 + wm-ts-nav/nav | 10 + wm-ts-nav/src/db.rs | 422 ++++++++++++++++ wm-ts-nav/src/indexer.rs | 102 ++++ wm-ts-nav/src/main.rs | 270 ++++++++++ wm-ts-nav/src/parser.rs | 716 +++++++++++++++++++++++++++ 18 files changed, 2351 insertions(+), 490 deletions(-) create mode 100644 .claude/skills/local-review/SKILL.md delete mode 100755 scripts/wm-cursor create mode 100644 wm-ts-nav/Cargo.lock create mode 100644 wm-ts-nav/Cargo.toml create mode 100755 wm-ts-nav/nav create mode 100644 wm-ts-nav/src/db.rs create mode 100644 wm-ts-nav/src/indexer.rs create mode 100644 wm-ts-nav/src/main.rs create mode 100644 wm-ts-nav/src/parser.rs diff --git a/.claude/hooks/format-backend.sh b/.claude/hooks/format-backend.sh index d6077d7482..2b77f71432 100755 --- a/.claude/hooks/format-backend.sh +++ b/.claude/hooks/format-backend.sh @@ -13,8 +13,10 @@ fi # Check if the file is in the backend directory and is a Rust file if [[ "$FILE_PATH" == *"/backend/"* ]] && [[ "$FILE_PATH" =~ \.rs$ ]]; then cd "$CLAUDE_PROJECT_DIR/backend" || exit 0 - # Run rustfmt with config from rustfmt.toml (edition=2021) - rustfmt --config-path rustfmt.toml "$FILE_PATH" 2>/dev/null || true + # Run rustfmt, surface errors as context but don't block Claude + if rustfmt --config-path rustfmt.toml "$FILE_PATH" 2>&1; then + echo "Formatted $(basename "$FILE_PATH")" + fi fi exit 0 diff --git a/.claude/hooks/format-frontend.sh b/.claude/hooks/format-frontend.sh index d0b4f0559b..37c3f8d4ec 100755 --- a/.claude/hooks/format-frontend.sh +++ b/.claude/hooks/format-frontend.sh @@ -15,8 +15,10 @@ if [[ "$FILE_PATH" == *"/frontend/"* ]]; then # Check if it's a formattable file type if [[ "$FILE_PATH" =~ \.(ts|js|svelte|json|css|html|md)$ ]]; then cd "$CLAUDE_PROJECT_DIR/frontend" || exit 0 - # Run prettier silently, don't fail the hook if prettier fails - npx prettier --write "$FILE_PATH" 2>/dev/null || true + # Run prettier, surface errors as context but don't block Claude + if ./node_modules/.bin/prettier --plugin prettier-plugin-svelte --write "$FILE_PATH" 2>&1; then + echo "Formatted $(basename "$FILE_PATH")" + fi fi fi diff --git a/.claude/settings.json b/.claude/settings.json index cf8bfdd284..0596b17e91 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -28,6 +28,12 @@ "Bash(git show:*)", "Bash(git blame:*)", "Bash(cargo check:*)", + "Bash(cargo build --release:*)", + "Bash(sh wm-ts-nav/nav:*)", + "Bash(wm-ts-nav/nav:*)", + "Bash(./wm-ts-nav/nav:*)", + "Bash(wm-ts-nav/target/release/wm-ts-nav:*)", + "Bash(./wm-ts-nav/target/release/wm-ts-nav:*)", "mcp__ide__getDiagnostics", "Bash(npm run generate-backend-client:*)", "Bash(npm run check:*)", diff --git a/.claude/skills/local-review/SKILL.md b/.claude/skills/local-review/SKILL.md new file mode 100644 index 0000000000..f14f5608db --- /dev/null +++ b/.claude/skills/local-review/SKILL.md @@ -0,0 +1,98 @@ +--- +name: local-review +user_invocable: true +description: Code review a pull request for bugs and CLAUDE.md compliance. MUST use when asked to review code. +--- + +# Local Code Review Skill + +Review a pull request for real bugs and CLAUDE.md compliance violations. This review targets HIGH SIGNAL issues only. + +## Review Philosophy + +- **Only flag issues you are certain about.** If you are not sure an issue is real, do not flag it. False positives erode trust and waste reviewer time. +- Think like a senior engineer doing a final review — flag things that would cause incidents, not things that are merely imperfect. + +## What to Flag + +- Code that won't compile or parse (syntax errors, type errors, missing imports) +- Code that will definitely produce wrong results regardless of inputs +- Clear, unambiguous CLAUDE.md violations (quote the exact rule being violated) +- Security issues in introduced code (injection, auth bypass, data exposure) +- Incorrect logic that will fail in production + +## What NOT to Flag + +- Code style or quality concerns +- Potential issues that depend on specific inputs or runtime state +- Subjective suggestions or improvements +- Pre-existing issues not introduced by this PR +- Pedantic nitpicks a senior engineer wouldn't flag +- Issues a linter or type checker will catch +- General quality concerns unless explicitly prohibited in CLAUDE.md +- Issues silenced via lint ignore comments + +## Execution Steps + +1. **Determine the PR scope**: + - If an argument is provided, use it as the PR number or branch + - Otherwise, detect from the current branch vs main + - Run `gh pr view` if a PR exists, or use `git diff main...HEAD` + +2. **Find relevant CLAUDE.md files**: + - Read the root `CLAUDE.md` + - Check for CLAUDE.md files in directories containing changed files + +3. **Get the diff and metadata**: + - `gh pr diff` or `git diff main...HEAD` for the full diff + - `gh pr view` or `git log main..HEAD --oneline` for context + +4. **Read changed files** where the diff alone is insufficient to understand context + +5. **Review for**: + - CLAUDE.md compliance — check each rule against the changed code + - Bugs and logic errors — will this code work correctly? + - Security issues — injection, auth, data exposure in new code + +6. **Self-validate each finding**: Before reporting, ask yourself: + - "Is this definitely a real issue, not a false positive?" + - "Would a senior engineer flag this in review?" + - If the answer to either is no, discard the finding + +7. **Output findings** to the terminal (default) or post as PR comments (with `--comment` flag) + +## Output Format + +``` +## Code review + +Found N issues: + +1. () + + +2. () + +``` + +If no issues are found: + +``` +## Code review + +No issues found. Checked for bugs and CLAUDE.md compliance. +``` + +## Posting Comments (--comment flag) + +If the user passes `--comment`, post findings as inline PR comments using: + +```bash +gh pr review --comment --body "" +``` + +Or for inline comments on specific lines: + +```bash +gh api repos/{owner}/{repo}/pulls/{pr}/reviews -f body="" -f event="COMMENT" -f comments="[...]" +``` diff --git a/.claude/skills/pr/SKILL.md b/.claude/skills/pr/SKILL.md index f0c4e822e4..ab67b58748 100644 --- a/.claude/skills/pr/SKILL.md +++ b/.claude/skills/pr/SKILL.md @@ -33,6 +33,7 @@ Follow conventional commit format for the PR title: - Keep under 70 characters - Use lowercase, imperative mood - No period at the end +- If `*_ee.rs` files were modified, prefix with `[ee]`: `[ee] : ` ## PR Body Format @@ -85,3 +86,25 @@ Generated with [Claude Code](https://claude.com/claude-code) )" ``` 7. Return the PR URL to the user + +## EE Companion PR (when `*_ee.rs` files were modified) + +The `*_ee.rs` files in the windmill repo are **symlinks** to `windmill-ee-private` — changes won't appear in `git diff` of the windmill repo. Instead, check the EE repo for uncommitted or unpushed changes. + +Follow the full EE PR workflow in `docs/enterprise.md`. The key PR-specific details: + +1. Find the EE repo/worktree: see "Finding the EE Repo" in `docs/enterprise.md` +2. Check for changes: `git -C status --short` + - If there are no changes in the EE repo, skip this entire section +3. Follow steps 1–5 from the "EE PR Workflow" in `docs/enterprise.md` +4. Create the companion PR (title does NOT get the `[ee]` prefix): + ```bash + gh pr create --draft --repo windmill-labs/windmill-ee-private --title ": " --body "$(cat <<'EOF' + Companion PR for windmill-labs/windmill# + + --- + Generated with [Claude Code](https://claude.com/claude-code) + EOF + )" + ``` +5. Commit `ee-repo-ref.txt` and push the updated windmill branch diff --git a/.workmux.yaml b/.workmux.yaml index 46049109c0..b36a8f16bf 100644 --- a/.workmux.yaml +++ b/.workmux.yaml @@ -67,6 +67,7 @@ files: copy: - backend/.env - scripts/ + - wm-ts-nav/target/release/wm-ts-nav sandbox: enabled: false diff --git a/CLAUDE.md b/CLAUDE.md index fe22fae0f7..f77dbb0600 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ Open-source platform for internal tools, workflows, API integrations, background ## Workflow -1. **Understand**: Before coding, read relevant docs from `docs/` to understand the area you're changing +1. **Understand**: Before coding, use `wm-ts-nav` to explore (see Code Navigation below). Use `outline` to understand file structure, `body` to read specific symbols, `def`/`callers`/`callees` to trace code. Read `docs/` for domain context. 2. **Plan**: For non-trivial changes, use plan mode. For large features, break into reviewable stages 3. **Execute**: Follow coding patterns from skills (`rust-backend`, `svelte-frontend`) 4. **Validate**: After every change, run the appropriate checks per `docs/validation.md` @@ -15,6 +15,7 @@ Open-source platform for internal tools, workflows, API integrations, background - **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow - **Backend patterns**: use the `rust-backend` skill when writing Rust code - **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code. Do NOT edit svelte files unless you have read that skill. +- **Code review**: use `/local-review` to review a PR for bugs and CLAUDE.md compliance - **Domain guides**: `.claude/skills/native-trigger/` and `frontend/tutorial-system-guide.mdc` - **Brand/UI guidelines**: `frontend/brand-guidelines.md` @@ -49,8 +50,43 @@ let { my_prop = $bindable(default_value) }: { my_prop?: string } = $props() 2. **Create a `useMyPropState()` helper** — encapsulate the undefined-handling logic in a reusable function and call it higher in the component tree, so the child component always receives a defined value. +## Code Navigation + +`wm-ts-nav` is an AST-aware code navigator. Use **Grep** for regex/pattern search. Use **wm-ts-nav** for structural queries — it skips comments/strings and understands symbol boundaries. + +**Prefer wm-ts-nav over Read** to save context window: +- `outline ` instead of reading a full file — understand structure first, then `body` or Read for specifics +- `body "X"` instead of reading a full file to see one function/struct +- `refs "X" --caller` instead of reading files to find which function contains each reference +- `callers "X"` / `callees "X"` for call-graph questions + +```bash +NAV="sh wm-ts-nav/nav" +# Use --root backend for Rust, --root frontend/src for TS/Svelte +$NAV --root backend outline backend/path/to/file.rs # file structure +$NAV --root backend def "ServiceName" # find definition +$NAV --root backend body "decrypt_oauth_data" # extract source code +$NAV --root backend search "%" --parent ServiceName # methods on a type +$NAV --root backend search "Trigger" --kind struct # find by kind +$NAV --root backend refs "X" --file handler.rs --caller # scoped refs with caller +$NAV --root backend callers "X" # who calls X? +$NAV --root backend callees "X" # what does X call? +``` + +**Limitations** — syntax-level analysis, no type inference: +- Import paths are stored literally — `crate::X` and `super::X` pointing to the same type won't be linked +- Re-export chains (`pub use`) aren't followed — refs through different re-export paths won't connect +- Trait methods can't be resolved to their trait definition +- Nested `use` trees (`use foo::{bar::{A, B}, baz::C}`) aren't parsed correctly +- Glob imports (`use foo::*`) — refs won't show import origin +- Macro-generated symbols (e.g. `sqlx::FromRow`) — invisible to tree-sitter +- Single-char identifiers — intentionally filtered out of refs +- `callees` shows all identifiers in a function body, not just actual calls +- `import * as ns` namespace imports — member accesses through `ns.X` aren't resolved + ## Core Principles +- **Use `outline`/`body` to explore, then `Read` with offset/limit from the results before editing** — avoid reading full files - Search for existing code to reuse before writing new code - Follow established patterns in the codebase - Keep changes focused — don't refactor beyond what's asked diff --git a/README_WORKMUX_DEV.md b/README_WORKMUX_DEV.md index 0b113e47ee..20d16d0352 100644 --- a/README_WORKMUX_DEV.md +++ b/README_WORKMUX_DEV.md @@ -192,70 +192,6 @@ sandbox: This mounts both the main EE repo (used by the main worktree) and the EE worktrees directory (used by feature worktrees) into every sandbox container. -## Cursor SSH Integration (`wmc`) - -`wm-cursor` (aliased as `wmc`) gives each worktree its own Cursor SSH remote window with an independently-focused tmux session. All windows are visible in the status bar across all Cursor terminals, but each one is focused on its own worktree. - -This uses **grouped tmux sessions** — multiple sessions that share the same window list but track focus independently: - -``` -tmux session: main <-- your main Cursor terminal -tmux session: cursor-feat-a <-- Cursor window for feat-a (focused on wm-feat-a) -tmux session: cursor-feat-b <-- Cursor window for feat-b (focused on wm-feat-b) - \__ all three share the same windows in the status bar -``` - -### Setup - -Run once from inside tmux on the remote: - -```bash -./scripts/wm-cursor setup /home/hugo/projects/windmill -``` - -This: - -1. **Merges `.vscode/settings.json`** — adds the `wm-tmux` terminal profile (auto-attaches to the `main` tmux session), disables auto port forwarding, configures forwarding for ports 8000/3000/5432, and stops rust-analyzer from auto-starting. Existing settings are preserved. -2. **Creates `.vscode/tasks.json`** — auto-starts the dev database (`start-dev-db.sh`) when the folder opens. -3. **Adds `wmc` alias to `~/.zshrc`** — so you can use `wmc` from any tmux window. -4. **Adds `eval "$(wmc completions)"`** to `~/.zshrc` — provides tab-completion for subcommands and worktree names (for `open`, `open-ee`, and `close`). - -After setup, reopen Cursor's terminal to pick up the new profile. - -### Usage - -All commands run from inside a tmux session (i.e., from Cursor's integrated terminal after setup). - -**Create a new worktree + open Cursor:** - -```bash -wmc add -A -p "implement feature X" -``` - -This runs `workmux add`, creates a grouped tmux session, writes `.vscode/settings.json` in the worktree (with port forwarding matching the worktree's assigned ports), and opens a new Cursor window. - -**Open Cursor for an existing worktree:** - -```bash -wmc open my-feature -``` - -**Open the EE worktree in Cursor (no tmux session):** - -```bash -wmc open-ee my-feature -``` - -This finds the matching `windmill-ee-private__worktrees/` directory and opens it in a new Cursor window. - -**Close a worktree's Cursor window and tmux window (keeps the worktree):** - -```bash -wmc close my-feature -``` - -This kills the grouped tmux session and calls `workmux close` to close the tmux window. The worktree and branch are preserved. Grouped sessions are also automatically cleaned up when you `workmux rm` a worktree (via `scripts/worktree-cleanup`). - ## Cargo Features To build the backend with specific Cargo features (e.g., `enterprise`, `parquet`), pass them via `CARGO_FEATURES`. The backend pane reads this from `.env.local` and appends `--features ` to the `cargo watch` command. @@ -270,20 +206,6 @@ CARGO_FEATURES="enterprise,parquet" wm add my-feature This gets written to `.env.local` by the `post_create` hook (`scripts/worktree-env`), and the backend pane picks it up automatically. -**With `wmc` (wm-cursor):** - -Use the `--features` flag: - -```bash -# Create a new worktree with features -wmc add --features "enterprise,parquet" -A -p "implement feature X" - -# Open an existing worktree with different features -wmc open my-feature --features "enterprise,parquet" -``` - -The `--features` flag exports `CARGO_FEATURES` so the `post_create` hook writes it to `.env.local`. When using `wmc open`, it updates the existing `.env.local` with the new features. - ## Login Default credentials: `admin@windmill.dev` / `changeme` diff --git a/docs/enterprise.md b/docs/enterprise.md index bfed61a2e6..5e2b7c9b61 100644 --- a/docs/enterprise.md +++ b/docs/enterprise.md @@ -15,17 +15,22 @@ - Standard location: `~/windmill-ee-private` - Worktree location: `~/windmill-ee-private__worktrees//` +## Detecting EE Changes + +The `*_ee.rs` files in the windmill repo are symlinks — changes won't appear in `git diff` of the windmill repo. Check the EE repo directly: `git -C status --short` + ## EE PR Workflow (MUST DO when modifying `*_ee.rs` files) When you modify any `*_ee.rs` file and create a PR on windmill: -1. **Create a matching branch** in `windmill-ee-private` (same branch name) -2. **Commit and push** the `_ee.rs` changes in that branch -3. **Create a PR** on `windmill-ee-private` with a link to the companion windmill PR -4. **Update `ee-repo-ref.txt`**: Run `bash write_latest_ee_ref.sh` from `backend/` +1. **Prefix the windmill PR title** with `[ee]`: `[ee] : ` +2. **Create a matching branch** in `windmill-ee-private` (same branch name) +3. **Commit and push** the `_ee.rs` changes in that branch +4. **Create a companion PR** on `windmill-ee-private` with a link to the windmill PR (no `[ee]` prefix on this one) +5. **Update `ee-repo-ref.txt`**: Run `bash write_latest_ee_ref.sh` from `backend/` - **Verify** it wrote the correct commit hash from your branch, not from main (the script may fall back to `~/windmill-ee-private` on main) - If wrong, manually write the correct hash -5. **Commit `ee-repo-ref.txt`** in the windmill repo so CI picks up the correct EE ref +6. **Commit `ee-repo-ref.txt`** in the windmill repo so CI picks up the correct EE ref ## Validation diff --git a/scripts/wm-cursor b/scripts/wm-cursor deleted file mode 100755 index 3c47da1179..0000000000 --- a/scripts/wm-cursor +++ /dev/null @@ -1,401 +0,0 @@ -#!/usr/bin/env zsh -emulate -L zsh -setopt err_exit no_unset pipe_fail - -# wm-cursor: Manage Cursor SSH remote windows with grouped tmux sessions -# Each worktree gets its own Cursor window with an independently-focused -# grouped tmux session, sharing the same window list in the status bar. - -# --- Resolve script path (must be at top level, not inside a function) --- - -local script_path=${0:A} - -# --- Lazy Cursor CLI resolution (only when needed) --- - -local cursor_bin= - -resolve_cursor_cli() { - [[ -n $cursor_bin ]] && return 0 - - local -a cursor_bins=(~/.cursor-server/cli/servers/*/server/bin/remote-cli/cursor(NOm)) - if (( ${#cursor_bins} == 0 )); then - print -u2 "Error: Cursor remote CLI not found in ~/.cursor-server/cli/servers/" - exit 1 - fi - cursor_bin=${cursor_bins[1]} - - # Refresh Cursor IPC socket (tmux may hold a stale one) - # Multiple stale sockets may exist; probe to find a live one - local sock - for sock in /tmp/vscode-ipc-*.sock(NOm); do - if timeout 2 env VSCODE_IPC_HOOK_CLI=$sock $cursor_bin --status &>/dev/null; then - export VSCODE_IPC_HOOK_CLI=$sock - break - fi - done -} - -# --- Helper functions --- - -ensure_tmux() { - if [[ -z ${TMUX-} ]]; then - print -u2 "Error: Not inside a tmux session" - exit 1 - fi -} - -check_dev_db() { - if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^windmill-db-dev$'; then - print -u2 "Warning: windmill-db-dev container is not running" - fi -} - -setup_grouped_session() { - local handle=$1 worktree_path=$2 - local session_name=cursor-${handle} - - # Detect the current main tmux session - local main_session=$(tmux display-message -p '#S') - - # Create grouped session (shares windows with the main session) - if ! tmux has-session -t $session_name 2>/dev/null; then - tmux new-session -d -t $main_session -s $session_name - fi - - # Focus on the worktree's window - tmux select-window -t ${session_name}:wm-${handle} 2>/dev/null || true - - # Write .vscode/settings.json in the worktree if it doesn't already exist - local settings_file=${worktree_path}/.vscode/settings.json - if [[ ! -f $settings_file ]]; then - mkdir -p ${settings_file:h} - - # Read ports from .env.local if available - local env_file=${worktree_path}/.env.local - local ports_config="" - if [[ -f $env_file ]]; then - local backend_port frontend_port - source $env_file - backend_port=${BACKEND_PORT-} - frontend_port=${FRONTEND_PORT-} - if [[ -n $backend_port && -n $frontend_port ]]; then - ports_config=', - "remote.autoForwardPorts": true, - "remote.otherPortsAttributes": { - "onAutoForward": "ignore" - }, - "remote.portsAttributes": { - "'$backend_port'": { "label": "Backend", "onAutoForward": "silent" }, - "'$frontend_port'": { "label": "Frontend", "onAutoForward": "openBrowserOnce" } - }' - fi - fi - - cat > $settings_file < from args, exports CARGO_FEATURES, returns remaining args. -# Usage: parse_features_flag "$@"; set -- "${remaining_args[@]}" - -parse_flags() { - remaining_args=() - while (( $# )); do - case $1 in - --features) - if (( $# < 2 )); then - print -u2 "Error: --features requires a value" - exit 1 - fi - export CARGO_FEATURES=$2 - shift 2 - ;; - --features=*) - export CARGO_FEATURES=${1#--features=} - shift - ;; - --clone-db) - export WM_CLONE_DB=1 - shift - ;; - *) - remaining_args+=("$1") - shift - ;; - esac - done -} - -# --- Subcommands --- - -cmd_add() { - resolve_cursor_cli - ensure_tmux - check_dev_db - - parse_flags "$@" - set -- "${remaining_args[@]}" - - # Snapshot worktree list before - local -a before=("${(@f)$(git worktree list --porcelain | grep '^worktree ')}") - - workmux add -b "$@" - - # Diff to find the new entry - local -a after=("${(@f)$(git worktree list --porcelain | grep '^worktree ')}") - local -a new=(${after:|before}) - - if (( ${#new} == 0 )); then - print -u2 "Error: Could not detect new worktree path" - exit 1 - fi - - local new_path=${new[1]#worktree } - local handle=${new_path:t} - - print "New worktree: ${handle} at ${new_path}" - setup_grouped_session $handle $new_path - $cursor_bin -n $new_path - print "Opened Cursor for ${handle}" -} - -cmd_open() { - local name=${1:?Usage: wm-cursor open } - shift - - resolve_cursor_cli - ensure_tmux - check_dev_db - - parse_flags "$@" - set -- "${remaining_args[@]}" - - # Write CARGO_FEATURES to .env.local if specified - if [[ -n ${CARGO_FEATURES-} ]]; then - local wt_env=$(workmux path $name)/.env.local - if [[ -f $wt_env ]]; then - # Remove existing CARGO_FEATURES line and append new one - sed -i '/^CARGO_FEATURES=/d' $wt_env - echo "CARGO_FEATURES=$CARGO_FEATURES" >> $wt_env - fi - fi - - local wt_path=$(workmux path $name) - local prev_target=$(tmux display-message -p '#{session_name}:#{window_index}') - - workmux open $name "$@" - tmux select-window -t $prev_target - setup_grouped_session $name $wt_path - $cursor_bin -n $wt_path - print "Opened Cursor for ${name}" -} - -cmd_close() { - local name=${1:?Usage: wm-cursor close } - - tmux kill-session -t cursor-${name} 2>/dev/null || true - workmux close $name -} - -cmd_open_ee() { - local name=${1:?Usage: wm-cursor open-ee } - - resolve_cursor_cli - local wt_path=$(workmux path $name) - local main_repo_root="$(cd "$(git -C "$wt_path" rev-parse --git-common-dir 2>/dev/null)/.." && pwd)" - - # Find ee repo (same discovery logic as worktree-env) - local ee_repo="" candidate - for candidate in \ - "${main_repo_root:+${main_repo_root}/../windmill-ee-private}" \ - "${wt_path}/../windmill-ee-private" \ - "${HOME}/windmill-ee-private" \ - "${HOME}/projects/windmill-ee-private"; do - if [[ -n $candidate ]] && [[ -d $candidate ]]; then - ee_repo=${candidate:A} - break - fi - done - - if [[ -z $ee_repo ]]; then - print -u2 "Error: Could not find windmill-ee-private repo" - exit 1 - fi - - local ee_worktree_dir="${ee_repo}__worktrees/${name}" - if [[ ! -d $ee_worktree_dir ]]; then - print -u2 "Error: EE worktree not found at ${ee_worktree_dir}" - exit 1 - fi - - $cursor_bin -n $ee_worktree_dir - print "Opened Cursor for EE worktree: ${ee_worktree_dir}" -} - -cmd_setup() { - local repo_root=${1:?Usage: wm-cursor setup } - repo_root=${repo_root:A} - local vscode_dir=${repo_root}/.vscode - - mkdir -p $vscode_dir - - # --- tasks.json --- - local tasks_file=${vscode_dir}/tasks.json - local write_tasks=true - if [[ -f $tasks_file ]]; then - print -n "tasks.json already exists. Overwrite? [y/N] " - read -q || { print; write_tasks=false } - print - fi - if $write_tasks; then - cat > $tasks_file <<'TASKS' -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Start dev DB", - "type": "shell", - "command": "./start-dev-db.sh", - "options": { "shell": { "executable": "/bin/bash" } }, - "runOptions": { "runOn": "folderOpen" }, - "presentation": { "reveal": "silent", "close": true }, - "problemMatcher": [] - } - ] -} -TASKS - print "Wrote ${tasks_file}" - fi - - # --- settings.json (merge wm-cursor keys, preserve existing) --- - local settings_file=${vscode_dir}/settings.json - local wmc_settings=' - { - "rust-analyzer.initializeStopped": true, - "terminal.integrated.defaultProfile.linux": "wm-tmux", - "terminal.integrated.profiles.linux": { - "wm-tmux": { - "path": "tmux", - "args": ["new-session", "-A", "-s", "main"] - } - }, - "remote.autoForwardPorts": true, - "remote.otherPortsAttributes": { - "onAutoForward": "ignore" - }, - "remote.portsAttributes": { - "8000": { "label": "Backend", "onAutoForward": "silent" }, - "3000": { "label": "Frontend", "onAutoForward": "openBrowserOnce" }, - "5432": { "label": "PostgreSQL", "onAutoForward": "silent" } - } - }' - - if [[ -f $settings_file ]]; then - # Strip // comments so jq can parse, merge, then write back - local existing - existing=$(python3 -c ' -import json, re, sys -text = sys.stdin.read() -# Remove // comments only outside of strings -text = re.sub(r'"'"'("(?:[^"\\]|\\.)*")|//[^\n]*'"'"', lambda m: m.group(1) or "", text) -json.dump(json.loads(text), sys.stdout, indent=2) -' < $settings_file) - jq --argjson wmc "$wmc_settings" '. * $wmc' <<< "$existing" > ${settings_file}.tmp \ - && mv ${settings_file}.tmp $settings_file - print "Merged wm-cursor settings into ${settings_file}" - else - jq . <<< "$wmc_settings" > $settings_file - print "Created ${settings_file}" - fi - - # --- zsh alias + completion --- - local rc=${ZDOTDIR:-$HOME}/.zshrc - local alias_line="alias wmc=${(q)script_path}" - - if [[ -f $rc ]] && grep -qF 'alias wmc=' $rc; then - sed -i "s|^alias wmc=.*|${alias_line}|" $rc - print "Updated wmc alias in ${rc}" - else - print "\n# wm-cursor alias\n${alias_line}" >> $rc - print "Added wmc alias to ${rc}" - fi - - local eval_line='eval "$(wmc completions)"' - if ! grep -qF 'wmc completions' $rc; then - print "${eval_line}" >> $rc - print "Added completions eval to ${rc}" - fi -} - -cmd_completions() { - cat <<'COMP' -_wmc_worktree_names() { - local -a names - names=(${(f)"$(git worktree list --porcelain 2>/dev/null | sed -n 's|^worktree .*/||p' | tail -n +2)"}) - _describe 'worktree' names -} - -_wmc() { - local -a subcmds=( - 'add:Create worktree + open Cursor' - 'open:Open Cursor for existing worktree' - 'open-ee:Open EE worktree in Cursor' - 'close:Clean up grouped tmux session' - 'setup:Set up .vscode settings, tasks + wmc alias' - 'completions:Print zsh completions' - ) - - if (( CURRENT == 2 )); then - _describe 'subcommand' subcmds - else - case $words[2] in - open|open-ee|close) - _wmc_worktree_names - ;; - esac - fi -} - -compdef _wmc wmc wm-cursor -COMP -} - -# --- Main --- - -case ${1-} in - add) shift; cmd_add "$@" ;; - open) shift; cmd_open "$@" ;; - open-ee) shift; cmd_open_ee "$@" ;; - close) shift; cmd_close "$@" ;; - setup) shift; cmd_setup "$@" ;; - completions) cmd_completions ;; - *) - print -u2 "Usage: wm-cursor [args...]" - print -u2 "" - print -u2 "Subcommands:" - print -u2 " add [--features ] [workmux-add-args...] Create worktree + open Cursor" - print -u2 " open [--features ] Open Cursor for existing worktree" - print -u2 " open-ee Open EE worktree in Cursor" - print -u2 " close Clean up grouped tmux session" - print -u2 " setup Set up .vscode settings, tasks + wmc alias" - print -u2 " completions Print zsh completions (use with eval)" - print -u2 "" - print -u2 "Options:" - print -u2 " --features Cargo features for the backend (e.g. \"enterprise,parquet\")" - print -u2 " --clone-db Clone the main 'windmill' database instead of creating an empty one" - exit 1 - ;; -esac diff --git a/scripts/worktree-common.sh b/scripts/worktree-common.sh index 91ba00a5e1..598469aad7 100755 --- a/scripts/worktree-common.sh +++ b/scripts/worktree-common.sh @@ -80,6 +80,13 @@ wm_copy_dependencies() { && echo "CLI deps installed and client generated" \ || echo "WARNING: CLI setup failed" >&2 fi + + local nav_bin="${main_repo_root}/wm-ts-nav/target/release/wm-ts-nav" + if [[ -f "$nav_bin" ]]; then + mkdir -p "${repo_root}/wm-ts-nav/target/release" + cp "$nav_bin" "${repo_root}/wm-ts-nav/target/release/" + echo "Copied wm-ts-nav binary" + fi } wm_allow_direnv() { @@ -261,5 +268,4 @@ wm_shared_pre_remove() { fi fi - tmux kill-session -t "cursor-${wt_basename}" 2>/dev/null || true } diff --git a/wm-ts-nav/Cargo.lock b/wm-ts-nav/Cargo.lock new file mode 100644 index 0000000000..b0f1343e5d --- /dev/null +++ b/wm-ts-nav/Cargo.lock @@ -0,0 +1,620 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tree-sitter" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5387dffa7ffc7d2dae12b50c6f7aab8ff79d6210147c6613561fc3d474c6f75" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + +[[package]] +name = "tree-sitter-rust" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8ccb3e3a3495c8a943f6c3fd24c3804c471fd7f4f16087623c7fa4c0068e8a" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wm-ts-nav" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "ignore", + "rayon", + "rusqlite", + "serde", + "serde_json", + "tree-sitter", + "tree-sitter-rust", + "tree-sitter-typescript", +] + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/wm-ts-nav/Cargo.toml b/wm-ts-nav/Cargo.toml new file mode 100644 index 0000000000..0dcf2c5081 --- /dev/null +++ b/wm-ts-nav/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "wm-ts-nav" +version = "0.1.0" +edition = "2021" + +[dependencies] +tree-sitter = "0.24" +tree-sitter-rust = "0.23" +tree-sitter-typescript = "0.23" + +rusqlite = { version = "0.32", features = ["bundled"] } +rayon = "1.10" +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +anyhow = "1" +ignore = "0.4" + +[profile.release] +opt-level = 2 +lto = "thin" diff --git a/wm-ts-nav/nav b/wm-ts-nav/nav new file mode 100755 index 0000000000..3d0bbf3557 --- /dev/null +++ b/wm-ts-nav/nav @@ -0,0 +1,10 @@ +#!/bin/sh +# Auto-rebuilding wrapper for wm-ts-nav +DIR="$(cd "$(dirname "$0")" && pwd)" +BIN="$DIR/target/release/wm-ts-nav" + +if [ ! -f "$BIN" ] || [ -n "$(find "$DIR/src" "$DIR/Cargo.toml" -newer "$BIN" 2>/dev/null | head -1)" ]; then + cargo build --release --manifest-path "$DIR/Cargo.toml" >&2 || exit 1 +fi + +exec "$BIN" "$@" diff --git a/wm-ts-nav/src/db.rs b/wm-ts-nav/src/db.rs new file mode 100644 index 0000000000..eb6556262c --- /dev/null +++ b/wm-ts-nav/src/db.rs @@ -0,0 +1,422 @@ +use anyhow::{Context, Result}; +use rusqlite::{params, Connection}; +use std::path::{Path, PathBuf}; +use std::time::SystemTime; + +use crate::parser::{IdentRef, Symbol}; + +pub struct Db { + conn: Connection, +} + +impl Db { + pub fn open(cache_dir: &Path) -> Result { + std::fs::create_dir_all(cache_dir) + .with_context(|| format!("creating cache dir: {}", cache_dir.display()))?; + let db_path = cache_dir.join("index.db"); + let conn = Connection::open(&db_path) + .with_context(|| format!("opening db: {}", db_path.display()))?; + + conn.execute_batch( + "PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + CREATE TABLE IF NOT EXISTS files ( + id INTEGER PRIMARY KEY, + path TEXT NOT NULL UNIQUE, + mtime_secs INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS symbols ( + id INTEGER PRIMARY KEY, + file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, + name TEXT NOT NULL, + kind TEXT NOT NULL, + line INTEGER NOT NULL, + end_line INTEGER NOT NULL, + signature TEXT, + parent TEXT + ); + CREATE TABLE IF NOT EXISTS refs ( + file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, + name TEXT NOT NULL, + line INTEGER NOT NULL, + import_path TEXT + ); + CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name); + CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id); + CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind); + CREATE INDEX IF NOT EXISTS idx_files_path ON files(path); + CREATE INDEX IF NOT EXISTS idx_refs_name ON refs(name); + CREATE INDEX IF NOT EXISTS idx_refs_file ON refs(file_id);", + )?; + + Ok(Self { conn }) + } + + pub fn begin(&self) -> Result<()> { + self.conn.execute_batch("BEGIN")?; + Ok(()) + } + + pub fn commit(&self) -> Result<()> { + self.conn.execute_batch("COMMIT")?; + Ok(()) + } + + pub fn upsert_file( + &self, + path: &str, + mtime_secs: i64, + symbols: &[Symbol], + refs: &[IdentRef], + ) -> Result<()> { + // Delete old entry if exists + self.conn.execute( + "DELETE FROM refs WHERE file_id IN (SELECT id FROM files WHERE path = ?1)", + params![path], + )?; + self.conn.execute( + "DELETE FROM symbols WHERE file_id IN (SELECT id FROM files WHERE path = ?1)", + params![path], + )?; + self.conn + .execute("DELETE FROM files WHERE path = ?1", params![path])?; + + // Insert new file + self.conn.execute( + "INSERT INTO files (path, mtime_secs) VALUES (?1, ?2)", + params![path, mtime_secs], + )?; + let file_id = self.conn.last_insert_rowid(); + + // Insert symbols + let mut stmt = self.conn.prepare_cached( + "INSERT INTO symbols (file_id, name, kind, line, end_line, signature, parent) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + )?; + for sym in symbols { + stmt.execute(params![ + file_id, + sym.name, + sym.kind, + sym.line, + sym.end_line, + sym.signature, + sym.parent, + ])?; + } + + // Insert refs + let mut ref_stmt = self.conn.prepare_cached( + "INSERT INTO refs (file_id, name, line, import_path) VALUES (?1, ?2, ?3, ?4)", + )?; + for r in refs { + ref_stmt.execute(params![file_id, r.name, r.line, r.import_path])?; + } + + Ok(()) + } + + pub fn remove_file(&self, path: &str) -> Result<()> { + self.conn.execute( + "DELETE FROM refs WHERE file_id IN (SELECT id FROM files WHERE path = ?1)", + params![path], + )?; + self.conn.execute( + "DELETE FROM symbols WHERE file_id IN (SELECT id FROM files WHERE path = ?1)", + params![path], + )?; + self.conn + .execute("DELETE FROM files WHERE path = ?1", params![path])?; + Ok(()) + } + + pub fn all_indexed_paths(&self) -> Result> { + let mut stmt = self + .conn + .prepare("SELECT path, mtime_secs FROM files")?; + let rows = stmt + .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? + .collect::, _>>()?; + Ok(rows) + } + + pub fn search_symbols( + &self, + pattern: &str, + kind_filter: Option<&str>, + parent_filter: Option<&str>, + limit: usize, + ) -> Result> { + let mut conditions = vec!["s.name LIKE ?1".to_string()]; + if let Some(kind) = kind_filter { + conditions.push(format!("s.kind = '{kind}'")); + } + if let Some(parent) = parent_filter { + conditions.push(format!("s.parent LIKE '%{parent}%'")); + } + let where_clause = conditions.join(" AND "); + let query = format!( + "SELECT s.name, s.kind, s.line, s.end_line, s.signature, s.parent, f.path + FROM symbols s JOIN files f ON s.file_id = f.id + WHERE {where_clause} + ORDER BY s.name LIMIT ?2" + ); + + let like_pattern = if pattern.contains('%') || pattern.contains('_') { + pattern.to_string() + } else { + format!("%{pattern}%") + }; + + let mut stmt = self.conn.prepare(&query)?; + let rows = stmt + .query_map(params![like_pattern, limit as i64], |row| { + Ok(SearchResult { + name: row.get(0)?, + kind: row.get(1)?, + line: row.get(2)?, + end_line: row.get(3)?, + signature: row.get(4)?, + parent: row.get(5)?, + path: row.get(6)?, + }) + })? + .collect::, _>>()?; + Ok(rows) + } + + pub fn file_symbols(&self, path: &str) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT s.name, s.kind, s.line, s.end_line, s.signature, s.parent, f.path + FROM symbols s JOIN files f ON s.file_id = f.id + WHERE f.path = ?1 + ORDER BY s.line", + )?; + let rows = stmt + .query_map(params![path], |row| { + Ok(SearchResult { + name: row.get(0)?, + kind: row.get(1)?, + line: row.get(2)?, + end_line: row.get(3)?, + signature: row.get(4)?, + parent: row.get(5)?, + path: row.get(6)?, + }) + })? + .collect::, _>>()?; + Ok(rows) + } + + pub fn find_refs( + &self, + name: &str, + limit: usize, + file_filter: Option<&str>, + with_caller: bool, + ) -> Result> { + let mut conditions = vec!["r.name = ?1".to_string()]; + if let Some(file) = file_filter { + conditions.push(format!("f.path LIKE '%{}'", file.replace('\'', "''"))); + } + let where_clause = conditions.join(" AND "); + + if with_caller { + let query = format!( + "SELECT path, line, import_path, caller_name, caller_kind FROM ( + SELECT f.path, r.line, r.import_path, s.name AS caller_name, s.kind AS caller_kind, + ROW_NUMBER() OVER ( + PARTITION BY r.file_id, r.line + ORDER BY (s.end_line - s.line) ASC + ) AS rn + FROM refs r + JOIN files f ON r.file_id = f.id + LEFT JOIN symbols s ON s.file_id = r.file_id + AND s.line <= r.line AND r.line <= s.end_line + AND s.kind IN ('function', 'impl', 'class', 'interface', 'method') + WHERE {where_clause} + ) WHERE rn = 1 + ORDER BY path, line + LIMIT ?2" + ); + let mut stmt = self.conn.prepare(&query)?; + let rows = stmt + .query_map(params![name, limit as i64], |row| { + Ok(RefResult { + path: row.get(0)?, + line: row.get(1)?, + import_path: row.get(2)?, + caller_name: row.get(3)?, + caller_kind: row.get(4)?, + }) + })? + .collect::, _>>()?; + Ok(rows) + } else { + let query = format!( + "SELECT f.path, r.line, r.import_path + FROM refs r JOIN files f ON r.file_id = f.id + WHERE {where_clause} + ORDER BY f.path, r.line + LIMIT ?2" + ); + let mut stmt = self.conn.prepare(&query)?; + let rows = stmt + .query_map(params![name, limit as i64], |row| { + Ok(RefResult { + path: row.get(0)?, + line: row.get(1)?, + import_path: row.get(2)?, + caller_name: None, + caller_kind: None, + }) + })? + .collect::, _>>()?; + Ok(rows) + } + } + + pub fn find_callers(&self, name: &str, limit: usize) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT caller_name, caller_kind, caller_line, caller_end_line, path, ref_line FROM ( + SELECT s.name AS caller_name, s.kind AS caller_kind, + s.line AS caller_line, s.end_line AS caller_end_line, + f.path, r.line AS ref_line, + ROW_NUMBER() OVER ( + PARTITION BY r.file_id, r.line + ORDER BY (s.end_line - s.line) ASC + ) AS rn + FROM refs r + JOIN symbols s ON s.file_id = r.file_id + AND s.line <= r.line AND r.line <= s.end_line + AND s.kind IN ('function', 'impl', 'class', 'interface', 'method') + JOIN files f ON r.file_id = f.id + WHERE r.name = ?1 + ) WHERE rn = 1 + ORDER BY path, caller_line + LIMIT ?2", + )?; + let rows = stmt + .query_map(params![name, limit as i64], |row| { + Ok(CallerResult { + caller_name: row.get(0)?, + caller_kind: row.get(1)?, + caller_line: row.get(2)?, + caller_end_line: row.get(3)?, + path: row.get(4)?, + ref_line: row.get(5)?, + }) + })? + .collect::, _>>()?; + Ok(rows) + } + + pub fn find_callees( + &self, + name: &str, + kind_filter: Option<&str>, + file_filter: Option<&str>, + ) -> Result> { + // First find the symbol + let results = self.search_symbols(name, kind_filter, None, 100)?; + let exact: Vec<_> = results.into_iter().filter(|r| r.name == name).collect(); + if exact.is_empty() { + return Ok(vec![]); + } + + let mut all_callees = Vec::new(); + for sym in &exact { + if let Some(file) = file_filter { + if !sym.path.contains(file) { + continue; + } + } + let mut stmt = self.conn.prepare( + "SELECT DISTINCT r.name, r.import_path + FROM refs r + JOIN files f ON r.file_id = f.id + WHERE f.path = ?1 AND r.line >= ?2 AND r.line <= ?3 + ORDER BY r.name", + )?; + let rows = stmt + .query_map(params![sym.path, sym.line, sym.end_line], |row| { + Ok(CalleeResult { + name: row.get(0)?, + import_path: row.get(1)?, + }) + })? + .collect::, _>>()?; + all_callees.extend(rows); + } + // Deduplicate by name + all_callees.sort_by(|a, b| a.name.cmp(&b.name)); + all_callees.dedup_by(|a, b| a.name == b.name); + Ok(all_callees) + } +} + +#[derive(Debug, serde::Serialize)] +pub struct RefResult { + pub path: String, + pub line: i64, + pub import_path: Option, + pub caller_name: Option, + pub caller_kind: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct CallerResult { + pub caller_name: String, + pub caller_kind: String, + pub caller_line: i64, + pub caller_end_line: i64, + pub path: String, + pub ref_line: i64, +} + +#[derive(Debug, serde::Serialize)] +pub struct CalleeResult { + pub name: String, + pub import_path: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct SearchResult { + pub name: String, + pub kind: String, + pub line: i64, + pub end_line: i64, + pub signature: Option, + pub parent: Option, + pub path: String, +} + +pub fn mtime_secs(path: &Path) -> Result { + let meta = std::fs::metadata(path)?; + let mtime = meta + .modified()? + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default(); + Ok(mtime.as_secs() as i64) +} + +pub fn cache_dir_for(root: &Path) -> PathBuf { + let hash = { + let s = root.to_string_lossy(); + let mut h: u64 = 5381; + for b in s.bytes() { + h = h.wrapping_mul(33).wrapping_add(b as u64); + } + h + }; + dirs_cache().join(format!("{hash:x}")) +} + +fn dirs_cache() -> PathBuf { + if let Ok(d) = std::env::var("XDG_CACHE_HOME") { + PathBuf::from(d).join("wm-ts-nav") + } else if let Ok(d) = std::env::var("HOME") { + PathBuf::from(d).join(".cache").join("wm-ts-nav") + } else { + PathBuf::from("/tmp/wm-ts-nav") + } +} diff --git a/wm-ts-nav/src/indexer.rs b/wm-ts-nav/src/indexer.rs new file mode 100644 index 0000000000..ef69fbd1f4 --- /dev/null +++ b/wm-ts-nav/src/indexer.rs @@ -0,0 +1,102 @@ +use anyhow::Result; +use ignore::WalkBuilder; +use rayon::prelude::*; +use std::collections::HashSet; +use std::path::Path; + +use crate::db::{self, Db}; +use crate::parser::{self, Lang}; + +pub struct IndexStats { + pub files_scanned: usize, + pub files_updated: usize, + pub files_removed: usize, + pub files_unchanged: usize, +} + +/// Incrementally update the index for the given root directory. +/// Only re-parses files whose mtime has changed since last index. +pub fn update_index(db: &Db, root: &Path) -> Result { + // Collect all supported files using `ignore` crate (respects .gitignore) + let files: Vec<_> = WalkBuilder::new(root) + .hidden(true) + .git_ignore(true) + .git_global(false) + .build() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false)) + .filter(|e| Lang::from_path(e.path()).is_some()) + .map(|e| e.into_path()) + .collect(); + + let disk_paths: HashSet = files + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + + // Check which files need updating + let existing = db.all_indexed_paths()?; + // Remove files no longer on disk + let mut files_removed = 0; + db.begin()?; + for (path, _) in &existing { + if !disk_paths.contains(path) { + db.remove_file(path)?; + files_removed += 1; + } + } + db.commit()?; + + // Figure out which files need re-parsing + let existing_map: std::collections::HashMap<&str, i64> = existing + .iter() + .map(|(p, m)| (p.as_str(), *m)) + .collect(); + + let to_parse: Vec<_> = files + .iter() + .filter(|path| { + let path_str = path.to_string_lossy(); + match existing_map.get(path_str.as_ref()) { + Some(&old_mtime) => { + // Check if mtime changed + db::mtime_secs(path).unwrap_or(0) != old_mtime + } + None => true, // New file + } + }) + .collect(); + + let files_unchanged = files.len() - to_parse.len(); + + // Parse files in parallel + let results: Vec<_> = to_parse + .par_iter() + .filter_map(|path| { + let mtime = db::mtime_secs(path).ok()?; + match parser::parse_file(path) { + Ok(result) => Some((path.to_string_lossy().to_string(), mtime, result)), + Err(e) => { + eprintln!("warning: failed to parse {}: {e}", path.display()); + None + } + } + }) + .collect(); + + let files_updated = results.len(); + + // Write to db in a single transaction + db.begin()?; + for (path, mtime, result) in &results { + db.upsert_file(path, *mtime, &result.symbols, &result.refs)?; + } + db.commit()?; + + Ok(IndexStats { + files_scanned: files.len(), + files_updated, + files_removed, + files_unchanged, + }) +} diff --git a/wm-ts-nav/src/main.rs b/wm-ts-nav/src/main.rs new file mode 100644 index 0000000000..8ae8c11f5b --- /dev/null +++ b/wm-ts-nav/src/main.rs @@ -0,0 +1,270 @@ +mod db; +mod indexer; +mod parser; + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(name = "wm-ts-nav", about = "Tree-sitter code navigator for Windmill")] +struct Cli { + /// Root directory to index (defaults to current directory) + #[arg(short, long)] + root: Option, + + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Index/re-index the codebase + Index, + /// Show symbols in a file + Outline { + /// File path + file: PathBuf, + }, + /// Search symbols by name pattern + Search { + /// Name pattern (supports SQL LIKE % wildcards) + pattern: String, + /// Filter by kind (function, struct, enum, trait, impl, etc.) + #[arg(short, long)] + kind: Option, + /// Filter by parent (e.g. --parent ServiceName to find methods on that type) + #[arg(short, long)] + parent: Option, + /// Max results + #[arg(short, long, default_value = "50")] + limit: usize, + }, + /// Find symbol definition by exact name + Def { + /// Exact symbol name + name: String, + /// Filter by kind + #[arg(short, long)] + kind: Option, + }, + /// Find references to a symbol in code (skips comments and strings) + Refs { + /// Symbol name to find + name: String, + /// Max results + #[arg(short, long, default_value = "50")] + limit: usize, + /// Filter to files matching this substring + #[arg(short, long)] + file: Option, + /// Show which function/symbol contains each reference + #[arg(short, long)] + caller: bool, + }, + /// Extract and print a symbol's source code + Body { + /// Exact symbol name + name: String, + /// Filter by kind + #[arg(short, long)] + kind: Option, + /// Filter to files matching this substring + #[arg(short, long)] + file: Option, + }, + /// Find what calls a symbol (who calls X?) + Callers { + /// Symbol name to find callers of + name: String, + /// Max results + #[arg(short, long, default_value = "50")] + limit: usize, + }, + /// Find what a symbol calls (what does X call?) + Callees { + /// Exact symbol name + name: String, + /// Filter by kind + #[arg(short, long)] + kind: Option, + /// Filter to files matching this substring + #[arg(short, long)] + file: Option, + }, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + let root = cli + .root + .unwrap_or_else(|| std::env::current_dir().expect("no cwd")); + let root = std::fs::canonicalize(&root)?; + let cache_dir = db::cache_dir_for(&root); + let db = db::Db::open(&cache_dir)?; + + // Always update index incrementally before any query + let stats = indexer::update_index(&db, &root)?; + + match cli.command { + Command::Index => { + println!( + "Indexed {} files: {} updated, {} unchanged, {} removed", + stats.files_scanned, stats.files_updated, stats.files_unchanged, stats.files_removed + ); + } + Command::Outline { file } => { + let file = std::fs::canonicalize(&file)?; + let symbols = db.file_symbols(&file.to_string_lossy())?; + if symbols.is_empty() { + println!("No symbols found"); + return Ok(()); + } + for s in &symbols { + let parent = s + .parent + .as_deref() + .map(|p| format!(" [{p}]")) + .unwrap_or_default(); + let sig = s + .signature + .as_deref() + .map(|s| format!(" {s}")) + .unwrap_or_default(); + println!("L{}-{} {:12} {}{}{}", s.line, s.end_line, s.kind, s.name, parent, sig); + } + } + Command::Search { + pattern, + kind, + parent, + limit, + } => { + let results = db.search_symbols(&pattern, kind.as_deref(), parent.as_deref(), limit)?; + if results.is_empty() { + println!("No symbols matching '{pattern}'"); + return Ok(()); + } + for r in &results { + let sig = r + .signature + .as_deref() + .map(|s| format!(" {s}")) + .unwrap_or_default(); + let parent_info = r + .parent + .as_deref() + .map(|p| format!(" [{p}]")) + .unwrap_or_default(); + println!("{}:{} {:12} {}{}{}", r.path, r.line, r.kind, r.name, parent_info, sig); + } + } + Command::Def { name, kind } => { + let results = db.search_symbols(&name, kind.as_deref(), None, 100)?; + let exact: Vec<_> = results.iter().filter(|r| r.name == name).collect(); + if exact.is_empty() { + println!("No definition found for '{name}'"); + return Ok(()); + } + for r in &exact { + let sig = r + .signature + .as_deref() + .map(|s| format!("\n {s}")) + .unwrap_or_default(); + let parent = r + .parent + .as_deref() + .map(|p| format!(" [{p}]")) + .unwrap_or_default(); + println!( + "{}:L{}-{} {} {}{}{}", + r.path, r.line, r.end_line, r.kind, r.name, parent, sig + ); + } + } + Command::Refs { + name, + limit, + file, + caller, + } => { + let results = db.find_refs(&name, limit, file.as_deref(), caller)?; + if results.is_empty() { + println!("No references found for '{name}'"); + return Ok(()); + } + for r in &results { + let origin = r + .import_path + .as_deref() + .map(|p| format!(" ({p})")) + .unwrap_or_default(); + let caller_info = r + .caller_name + .as_deref() + .map(|c| format!(" [{c}]")) + .unwrap_or_default(); + println!("{}:{}{}{}", r.path, r.line, caller_info, origin); + } + } + Command::Body { name, kind, file } => { + let results = db.search_symbols(&name, kind.as_deref(), None, 100)?; + let mut exact: Vec<_> = results.into_iter().filter(|r| r.name == name).collect(); + if let Some(ref f) = file { + exact.retain(|r| r.path.contains(f.as_str())); + } + if exact.is_empty() { + println!("No definition found for '{name}'"); + return Ok(()); + } + for (i, r) in exact.iter().enumerate() { + if i > 0 { + println!("\n---\n"); + } + println!("{}:L{}-{}", r.path, r.line, r.end_line); + match std::fs::read_to_string(&r.path) { + Ok(contents) => { + let lines: Vec<&str> = contents.lines().collect(); + let start = (r.line as usize).saturating_sub(1); + let end = (r.end_line as usize).min(lines.len()); + for line in &lines[start..end] { + println!("{line}"); + } + } + Err(e) => println!(" (error reading file: {e})"), + } + } + } + Command::Callers { name, limit } => { + let results = db.find_callers(&name, limit)?; + if results.is_empty() { + println!("No callers found for '{name}'"); + return Ok(()); + } + for r in &results { + println!( + "{}:L{}-{} {} {} → L{}", + r.path, r.caller_line, r.caller_end_line, r.caller_kind, r.caller_name, r.ref_line + ); + } + } + Command::Callees { name, kind, file } => { + let results = db.find_callees(&name, kind.as_deref(), file.as_deref())?; + if results.is_empty() { + println!("No callees found for '{name}'"); + return Ok(()); + } + for r in &results { + let origin = r + .import_path + .as_deref() + .map(|p| format!(" ({p})")) + .unwrap_or_default(); + println!("{}{}", r.name, origin); + } + } + } + + Ok(()) +} diff --git a/wm-ts-nav/src/parser.rs b/wm-ts-nav/src/parser.rs new file mode 100644 index 0000000000..43d3bfe0af --- /dev/null +++ b/wm-ts-nav/src/parser.rs @@ -0,0 +1,716 @@ +use anyhow::{Context, Result}; +use std::path::Path; +use tree_sitter::{Node, Parser}; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct Symbol { + pub name: String, + pub kind: String, + pub line: usize, + pub end_line: usize, + pub signature: Option, + pub parent: Option, +} + +#[derive(Debug, Clone)] +pub struct IdentRef { + pub name: String, + pub line: usize, + /// Resolved import path if known (e.g. "windmill_common::error::Error") + pub import_path: Option, +} + +/// A `use` import with its scope +#[derive(Debug, Clone)] +pub struct ImportEntry { + /// The short name (e.g. "Error") + pub name: String, + /// Full path (e.g. "windmill_common::error::Error") + pub full_path: String, + /// Line where the use is declared + pub line: usize, + /// End of the scope this use lives in (file end for top-level, block end for scoped) + pub scope_end: usize, +} + +pub struct ParseResult { + pub symbols: Vec, + pub refs: Vec, +} + +pub enum Lang { + Rust, + Typescript, + Tsx, +} + +impl Lang { + pub fn from_path(path: &Path) -> Option { + match path.extension()?.to_str()? { + "rs" => Some(Self::Rust), + "tsx" | "jsx" => Some(Self::Tsx), + "ts" | "js" => Some(Self::Typescript), + "svelte" => Some(Self::Typescript), // we extract {#if flowStore.val?.value?.failure_module} @@ -67,7 +71,7 @@ + {/if} + + {#if smallFailureModule} - + {/if}
{:else} @@ -124,14 +142,17 @@ {#snippet trigger()} {/snippet} diff --git a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte index eada672f24..fc8b68afb9 100644 --- a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte +++ b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte @@ -271,6 +271,8 @@ let sidebarMode: 'list' | 'graph' = 'graph' let minHeight = $state(0) + let flowPaneWidth = $state(0) + let compactTopbar = $derived(flowPaneWidth < 700) export function selectNextId(id: any) { if (flowStore.val.value.modules) { @@ -505,11 +507,12 @@ {/each} -
+
void disableAi?: boolean diffManager?: FlowDiffManager + compact?: boolean } let { @@ -33,7 +34,8 @@ noteMode, toggleNoteMode, disableAi, - diffManager + diffManager, + compact = false }: Props = $props() const { selectionManager, flowStore } = getContext('FlowEditorContext') @@ -42,23 +44,31 @@
{#if !disableSettings} - + + + {#snippet text()} + Settings + {/snippet} + {/if} - + {#snippet text()} Error Handler {/snippet} From bc35c946161629b17483130b9634de2416235cff Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 13 Mar 2026 12:29:58 +0000 Subject: [PATCH 23/25] ci --- .github/workflows/npm_on_release.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/npm_on_release.yml b/.github/workflows/npm_on_release.yml index 6aa537060e..ea8bdeb482 100644 --- a/.github/workflows/npm_on_release.yml +++ b/.github/workflows/npm_on_release.yml @@ -8,17 +8,21 @@ on: jobs: publish_typescript_client: runs-on: ubicloud-standard-8 + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: "20.x" registry-url: "https://registry.npmjs.org" - - run: cd typescript-client && ./publish.sh && cd .. - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: cd typescript-client && ./publish.sh --provenance --access public && cd .. publish_cli: runs-on: ubicloud-standard-8 + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 @@ -28,6 +32,4 @@ jobs: - uses: oven-sh/setup-bun@v2 with: bun-version: latest - - run: cd cli && ./build.sh && cd npm && npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: cd cli && ./build.sh && cd npm && npm publish --provenance --access public From 2a8e276b6d2761bb2798b6bc5f8d90ab34fbb403 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 13 Mar 2026 12:38:41 +0000 Subject: [PATCH 24/25] fix(ci): add NODE_AUTH_TOKEN for npm publish authentication Co-Authored-By: Claude Opus 4.6 --- .github/workflows/npm_on_release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/npm_on_release.yml b/.github/workflows/npm_on_release.yml index ea8bdeb482..5f84cb9ae8 100644 --- a/.github/workflows/npm_on_release.yml +++ b/.github/workflows/npm_on_release.yml @@ -18,6 +18,8 @@ jobs: node-version: "20.x" registry-url: "https://registry.npmjs.org" - run: cd typescript-client && ./publish.sh --provenance --access public && cd .. + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} publish_cli: runs-on: ubicloud-standard-8 permissions: @@ -33,3 +35,5 @@ jobs: with: bun-version: latest - run: cd cli && ./build.sh && cd npm && npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 44dd3ee8cd05d288828d1d46c84cbcdf40f8fa78 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 13 Mar 2026 12:43:45 +0000 Subject: [PATCH 25/25] fix(ci): remove provenance flag and use NPM_TOKEN for npm publish Co-Authored-By: Claude Opus 4.6 --- .github/workflows/npm_on_release.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/npm_on_release.yml b/.github/workflows/npm_on_release.yml index 5f84cb9ae8..a41bd80854 100644 --- a/.github/workflows/npm_on_release.yml +++ b/.github/workflows/npm_on_release.yml @@ -8,23 +8,17 @@ on: jobs: publish_typescript_client: runs-on: ubicloud-standard-8 - permissions: - contents: read - id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: "20.x" registry-url: "https://registry.npmjs.org" - - run: cd typescript-client && ./publish.sh --provenance --access public && cd .. + - run: cd typescript-client && ./publish.sh --access public && cd .. env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} publish_cli: runs-on: ubicloud-standard-8 - permissions: - contents: read - id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 @@ -34,6 +28,6 @@ jobs: - uses: oven-sh/setup-bun@v2 with: bun-version: latest - - run: cd cli && ./build.sh && cd npm && npm publish --provenance --access public + - run: cd cli && ./build.sh && cd npm && npm publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}