import * as dntShim from "../../../_dnt.shims.js";
import { requireLogin } from "../../core/auth.js";
import { fetchVersion, resolveWorkspace } from "../../core/context.js";
import { colors, Command, Confirm, ensureDir, minimatch, path, log, yamlStringify, yamlParseContent, SEP, } from "../../../deps.js";
import * as wmill from "../../../gen/services.gen.js";
import { getTypeStrFromPath, parseFromPath, pushObj, showConflict, showDiff, } from "../../types.js";
import { downloadZip } from "./pull.js";
import { exts, findContentFile, findGlobalDeps, findResourceFile, handleScriptMetadata, removeExtensionToPath, } from "../script/script.js";
import { handleFile } from "../script/script.js";
import { deepEqual, isFileResource } from "../../utils/utils.js";
import { getEffectiveSettings, validateBranchConfiguration, mergeConfigWithConfigFile, } from "../../core/conf.js";
import { getSpecificItemsForCurrentBranch, isSpecificItem, getBranchSpecificPath, fromBranchSpecificPath, isCurrentBranchFile, isBranchSpecificFile, } from "../../core/specific_items.js";
import { getCurrentGitBranch } from "../../utils/git.js";
import { removePathPrefix } from "../../types.js";
import { listSyncCodebases } from "../../utils/codebase.js";
import { generateFlowLockInternal, generateScriptMetadataInternal, readLockfile, } from "../../utils/metadata.js";
import { pushResource } from "../resource/resource.js";
import { newPathAssigner, } from "../../../windmill-utils-internal/src/path-utils/path-assigner.js";
import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../windmill-utils-internal/src/inline-scripts/extractor.js";
// Merge CLI options with effective settings, preserving CLI flags as overrides
function mergeCliWithEffectiveOptions(cliOpts, effectiveOpts) {
    // overlay CLI options on top (undefined cliOpts won't override effectiveOpts)
    return Object.assign({}, effectiveOpts, cliOpts);
}
// Resolve effective sync options using branch-based configuration
async function resolveEffectiveSyncOptions(workspace, localConfig, promotion) {
    return await getEffectiveSettings(localConfig, promotion);
}
export function findCodebase(path, codebases) {
    if (!path.endsWith(".ts")) {
        return;
    }
    for (const c of codebases) {
        let included = false;
        let excluded = false;
        if (c.includes == undefined || c.includes == null) {
            included = true;
        }
        if (typeof c.includes == "string") {
            c.includes = [c.includes];
        }
        for (const r of c.includes ?? []) {
            if (included) {
                break;
            }
            if (minimatch(path, r)) {
                included = true;
            }
        }
        if (typeof c.excludes == "string") {
            c.excludes = [c.excludes];
        }
        for (const r of c.excludes ?? []) {
            if (minimatch(path, r)) {
                excluded = true;
            }
        }
        if (included && !excluded) {
            return c;
        }
    }
}
async function addCodebaseDigestIfRelevant(path, content, codebases, ignoreCodebaseChanges) {
    const isScript = path.endsWith(".script.yaml");
    if (!isScript) {
        return content;
    }
    let isTs = true;
    const replacedPath = path.replace(".script.yaml", ".ts");
    try {
        await dntShim.Deno.stat(replacedPath);
    }
    catch {
        isTs = false;
    }
    if (!isTs) {
        return content;
    }
    if (isTs) {
        const c = findCodebase(replacedPath, codebases);
        if (c) {
            let parsed;
            try {
                parsed = yamlParseContent(path, content);
            }
            catch (error) {
                log.error(`Failed to parse YAML content for codebase digest at path: ${path}`);
                throw error;
            }
            if (parsed && typeof parsed == "object") {
                if (ignoreCodebaseChanges) {
                    parsed["codebase"] = undefined;
                }
                else {
                    parsed["codebase"] = await c.getDigest();
                }
                parsed["lock"] = "";
                return yamlStringify(parsed, yamlOptions);
            }
            else {
                throw Error(`Expected local yaml ${path} to be an object, found: ${content} instead`);
            }
        }
    }
    return content;
}
export async function FSFSElement(p, codebases, ignoreCodebaseChanges) {
    function _internal_element(localP, isDir, codebases) {
        return {
            isDirectory: isDir,
            path: localP.substring(p.length + 1),
            async *getChildren() {
                if (!isDir)
                    return [];
                try {
                    for await (const e of dntShim.Deno.readDir(localP)) {
                        yield _internal_element(path.join(localP, e.name), e.isDirectory, codebases);
                    }
                }
                catch (e) {
                    log.warn(`Error reading dir: ${localP}, ${e}`);
                }
            },
            // async getContentBytes(): Promise<Uint8Array> {
            //   return await Deno.readFile(localP);
            // },
            async getContentText() {
                const content = await dntShim.Deno.readTextFile(localP);
                const itemPath = localP.substring(p.length + 1);
                const r = await addCodebaseDigestIfRelevant(itemPath, content, codebases, ignoreCodebaseChanges);
                return r;
            },
        };
    }
    return _internal_element(p, (await dntShim.Deno.stat(p)).isDirectory, codebases);
}
function prioritizeName(name) {
    if (name == "version")
        return "aaa";
    if (name == "id")
        return "aa";
    if (name == "type")
        return "ab";
    if (name == "summary")
        return "ad";
    if (name == "name")
        return "ae";
    if (name == "display_name")
        return "af";
    if (name == "description")
        return "ag";
    if (name == "value")
        return "ah";
    if (name == "content")
        return "ai";
    if (name == "modules")
        return "aj";
    if (name == "failure_module")
        return "ak";
    if (name == "input_transforms")
        return "al";
    if (name == "lock")
        return "az";
    if (name == "locks")
        return "azz";
    return name;
}
export const yamlOptions = {
    sortKeys: (a, b) => {
        return prioritizeName(a).localeCompare(prioritizeName(b));
    },
    noCompatMode: true,
    noRefs: true,
    skipInvalid: true,
};
export function extractInlineScriptsForApps(rec, pathAssigner) {
    if (!rec) {
        return [];
    }
    if (typeof rec == "object") {
        return Object.entries(rec).flatMap(([k, v]) => {
            if (k == "inlineScript" && typeof v == "object") {
                const o = v;
                const name = rec["name"];
                const [basePath, ext] = pathAssigner.assignPath(name, o["language"]);
                const r = [];
                if (o["content"]) {
                    const content = o["content"];
                    o["content"] = "!inline " + basePath.replaceAll(SEP, "/") + ext;
                    r.push({
                        path: basePath + ext,
                        content: content,
                    });
                }
                if (o["lock"] && o["lock"] != "") {
                    const lock = o["lock"];
                    o["lock"] = "!inline " + basePath.replaceAll(SEP, "/") + "lock";
                    r.push({
                        path: basePath + "lock",
                        content: lock,
                    });
                }
                return r;
            }
            else {
                return extractInlineScriptsForApps(v, pathAssigner);
            }
        });
    }
    return [];
}
function ZipFSElement(zip, useYaml, defaultTs, resourceTypeToFormatExtension, ignoreCodebaseChanges) {
    async function _internal_file(p, f) {
        const kind = p.endsWith("flow.json")
            ? "flow"
            : p.endsWith("app.json")
                ? "app"
                : p.endsWith("script.json")
                    ? "script"
                    : p.endsWith("resource.json")
                        ? "resource"
                        : "other";
        const isJson = p.endsWith(".json");
        function transformPath() {
            if (kind == "flow") {
                return p.replace("flow.json", "flow");
            }
            else if (kind == "app") {
                return p.replace("app.json", "app");
            }
            else {
                return useYaml && isJson ? p.replaceAll(".json", ".yaml") : p;
            }
        }
        const finalPath = transformPath();
        const r = [
            {
                isDirectory: kind == "flow" || kind == "app",
                path: finalPath,
                async *getChildren() {
                    if (kind == "flow") {
                        let flow;
                        try {
                            flow = JSON.parse(await f.async("text"));
                        }
                        catch (error) {
                            log.error(`Failed to parse flow.yaml at path: ${p}`);
                            throw error;
                        }
                        let inlineScripts;
                        try {
                            inlineScripts = extractInlineScriptsForFlows(flow.value.modules, {}, SEP, defaultTs);
                        }
                        catch (error) {
                            log.error(`Failed to extract inline scripts for flow at path: ${p}`);
                            throw error;
                        }
                        for (const s of inlineScripts) {
                            yield {
                                isDirectory: false,
                                path: path.join(finalPath, s.path),
                                async *getChildren() { },
                                // deno-lint-ignore require-await
                                async getContentText() {
                                    return s.content;
                                },
                            };
                        }
                        yield {
                            isDirectory: false,
                            path: path.join(finalPath, "flow.yaml"),
                            async *getChildren() { },
                            // deno-lint-ignore require-await
                            async getContentText() {
                                return yamlStringify(flow, yamlOptions);
                            },
                        };
                    }
                    else if (kind == "app") {
                        let app;
                        try {
                            app = JSON.parse(await f.async("text"));
                        }
                        catch (error) {
                            log.error(`Failed to parse app.yaml at path: ${p}`);
                            throw error;
                        }
                        let inlineScripts;
                        try {
                            inlineScripts = extractInlineScriptsForApps(app?.["value"], newPathAssigner(defaultTs));
                        }
                        catch (error) {
                            log.error(`Failed to extract inline scripts for app at path: ${p}`);
                            throw error;
                        }
                        for (const s of inlineScripts) {
                            yield {
                                isDirectory: false,
                                path: path.join(finalPath, s.path),
                                async *getChildren() { },
                                // deno-lint-ignore require-await
                                async getContentText() {
                                    return s.content;
                                },
                            };
                        }
                        yield {
                            isDirectory: false,
                            path: path.join(finalPath, "app.yaml"),
                            async *getChildren() { },
                            // deno-lint-ignore require-await
                            async getContentText() {
                                return yamlStringify(app, yamlOptions);
                            },
                        };
                    }
                },
                async getContentText() {
                    const content = await f.async("text");
                    if (kind == "script") {
                        let parsed;
                        try {
                            parsed = JSON.parse(content);
                        }
                        catch (error) {
                            log.error(`Failed to parse script.yaml at path: ${p}`);
                            throw error;
                        }
                        if (parsed["lock"] &&
                            parsed["lock"] != "" &&
                            parsed["codebase"] == undefined) {
                            parsed["lock"] =
                                "!inline " +
                                    removeSuffix(p.replaceAll(SEP, "/"), ".json") +
                                    ".lock";
                        }
                        else if (parsed["lock"] == "") {
                            parsed["lock"] = "";
                        }
                        else {
                            parsed["lock"] = undefined;
                        }
                        if (ignoreCodebaseChanges && parsed["codebase"]) {
                            parsed["codebase"] = undefined;
                        }
                        return useYaml
                            ? yamlStringify(parsed, yamlOptions)
                            : JSON.stringify(parsed, null, 2);
                    }
                    if (kind == "resource") {
                        const content = await f.async("text");
                        let parsed;
                        try {
                            parsed = JSON.parse(content);
                        }
                        catch (error) {
                            log.error(`Failed to parse resource.yaml at path: ${p}`);
                            throw error;
                        }
                        const formatExtension = resourceTypeToFormatExtension[parsed["resource_type"]];
                        if (formatExtension) {
                            parsed["value"]["content"] =
                                "!inline " +
                                    removeSuffix(p.replaceAll(SEP, "/"), ".resource.json") +
                                    ".resource.file." +
                                    formatExtension;
                        }
                        return useYaml
                            ? yamlStringify(parsed, yamlOptions)
                            : JSON.stringify(parsed, null, 2);
                    }
                    return useYaml && isJson
                        ? (() => {
                            try {
                                return yamlStringify(JSON.parse(content), yamlOptions);
                            }
                            catch (error) {
                                log.error(`Failed to parse JSON content at path: ${p}`);
                                throw error;
                            }
                        })()
                        : content;
                },
            },
        ];
        if (kind == "script") {
            const content = await f.async("text");
            let parsed;
            try {
                parsed = JSON.parse(content);
            }
            catch (error) {
                log.error(`Failed to parse script lock content at path: ${p}`);
                throw error;
            }
            const lock = parsed["lock"];
            if (lock && lock != "") {
                r.push({
                    isDirectory: false,
                    path: removeSuffix(finalPath, ".json") + ".lock",
                    async *getChildren() { },
                    // deno-lint-ignore require-await
                    async getContentText() {
                        return lock;
                    },
                });
            }
        }
        if (kind == "resource") {
            const content = await f.async("text");
            let parsed;
            try {
                parsed = JSON.parse(content);
            }
            catch (error) {
                log.error(`Failed to parse resource file content at path: ${p}`);
                throw error;
            }
            const formatExtension = resourceTypeToFormatExtension[parsed["resource_type"]];
            if (formatExtension) {
                const fileContent = parsed["value"]["content"];
                if (typeof fileContent === "string") {
                    r.push({
                        isDirectory: false,
                        path: removeSuffix(finalPath, ".resource.json") +
                            ".resource.file." +
                            formatExtension,
                        async *getChildren() { },
                        // deno-lint-ignore require-await
                        async getContentText() {
                            return fileContent;
                        },
                    });
                }
            }
        }
        return r;
    }
    function _internal_folder(p, zip) {
        return {
            isDirectory: true,
            path: p,
            async *getChildren() {
                for (const filename in zip.files) {
                    const file = zip.files[filename];
                    const totalPath = path.join(p, filename);
                    if (file.dir) {
                        const e = zip.folder(file.name);
                        yield _internal_folder(totalPath, e);
                    }
                    else {
                        const fs = await _internal_file(totalPath, file);
                        for (const f of fs) {
                            yield f;
                        }
                    }
                }
            },
            // // deno-lint-ignore require-await
            // async getContentBytes(): Promise<Uint8Array> {
            //   throw new Error("Cannot get content of folder");
            // },
            // deno-lint-ignore require-await
            async getContentText() {
                throw new Error("Cannot get content of folder");
            },
        };
    }
    return _internal_folder("." + SEP, zip);
}
export async function* readDirRecursiveWithIgnore(ignore, root) {
    const stack = [
        {
            path: root.path,
            ignored: ignore(root.path, root.isDirectory),
            isDirectory: root.isDirectory,
            c: root.getChildren,
            // getContentBytes(): Promise<Uint8Array> {
            //   throw undefined;
            // },
            getContentText() {
                throw undefined;
            },
        },
    ];
    while (stack.length > 0) {
        const e = stack.pop();
        // console.log(e.path);
        yield e;
        for await (const e2 of e.c()) {
            if (e2.isDirectory) {
                const dirName = e2.path.split(SEP).pop();
                if (dirName == "node_modules" || dirName?.startsWith(".")) {
                    continue;
                }
            }
            // console.log(e2.path);
            stack.push({
                path: e2.path,
                ignored: e.ignored || ignore(e2.path, e2.isDirectory),
                isDirectory: e2.isDirectory,
                // getContentBytes: e2.getContentBytes,
                getContentText: e2.getContentText,
                c: e2.getChildren,
            });
        }
    }
}
export async function elementsToMap(els, ignore, json, skips, specificItems) {
    const map = {};
    const processedBasePaths = new Set();
    // First pass: collect all file paths to identify branch-specific files
    const allPaths = [];
    for await (const entry of readDirRecursiveWithIgnore(ignore, els)) {
        if (!entry.isDirectory && !entry.ignored) {
            allPaths.push(entry.path);
        }
    }
    const branchSpecificExists = new Set();
    if (specificItems) {
        const currentBranch = getCurrentGitBranch();
        if (currentBranch) {
            for (const path of allPaths) {
                if (isCurrentBranchFile(path)) {
                    const basePath = fromBranchSpecificPath(path, currentBranch);
                    if (isSpecificItem(basePath, specificItems)) {
                        branchSpecificExists.add(basePath);
                    }
                }
            }
        }
    }
    for await (const entry of readDirRecursiveWithIgnore(ignore, els)) {
        if (entry.isDirectory || entry.ignored)
            continue;
        const path = entry.path;
        if (json && path.endsWith(".yaml") && !isFileResource(path))
            continue;
        if (!json && path.endsWith(".json") && !isFileResource(path))
            continue;
        const ext = json ? ".json" : ".yaml";
        if (!skips.includeSchedules && path.endsWith(".schedule" + ext))
            continue;
        if (!skips.includeTriggers &&
            (path.endsWith(".http_trigger" + ext) ||
                path.endsWith(".websocket_trigger" + ext) ||
                path.endsWith(".kafka_trigger" + ext) ||
                path.endsWith(".nats_trigger" + ext) ||
                path.endsWith(".postgres_trigger" + ext) ||
                path.endsWith(".mqtt_trigger" + ext) ||
                path.endsWith(".sqs_trigger" + ext) ||
                path.endsWith(".gcp_trigger" + ext) ||
                path.endsWith(".email_trigger" + ext)))
            continue;
        if (!skips.includeUsers && path.endsWith(".user" + ext))
            continue;
        if (!skips.includeGroups && path.endsWith(".group" + ext))
            continue;
        if (!skips.includeSettings && path === "settings" + ext)
            continue;
        if (!skips.includeKey && path === "encryption_key")
            continue;
        if (skips.skipResources && path.endsWith(".resource" + ext))
            continue;
        if (skips.skipResourceTypes && path.endsWith(".resource-type" + ext))
            continue;
        // Use getTypeStrFromPath for consistent type detection
        try {
            const fileType = getTypeStrFromPath(path);
            if (skips.skipVariables && fileType === "variable")
                continue;
            if (skips.skipScripts && fileType === "script")
                continue;
            if (skips.skipFlows && fileType === "flow")
                continue;
            if (skips.skipApps && fileType === "app")
                continue;
            if (skips.skipFolders && fileType === "folder")
                continue;
        }
        catch {
            // If getTypeStrFromPath can't determine the type, continue processing the file
        }
        if (skips.skipResources && isFileResource(path))
            continue;
        if (![
            "json",
            "yaml",
            "go",
            "sh",
            "ts",
            "py",
            "sql",
            "gql",
            "ps1",
            "php",
            "js",
            "lock",
            "rs",
            "cs",
            "yml",
            "nu",
            "java",
            "rb",
            // for related places search: ADD_NEW_LANG
        ].includes(path.split(".").pop() ?? "") &&
            !isFileResource(path))
            continue;
        // Handle branch-specific files - skip files for other branches
        if (specificItems && isBranchSpecificFile(path)) {
            const currentBranch = getCurrentGitBranch();
            if (!currentBranch || !isCurrentBranchFile(path)) {
                // Skip branch-specific files for other branches
                continue;
            }
        }
        const content = await entry.getContentText();
        if (skips.skipSecrets && path.endsWith(".variable" + ext)) {
            try {
                let o;
                if (json) {
                    try {
                        o = JSON.parse(content);
                    }
                    catch (error) {
                        log.error(`Failed to parse JSON variable content at path: ${path}`);
                        throw error;
                    }
                }
                else {
                    try {
                        o = yamlParseContent(path, content);
                    }
                    catch (error) {
                        log.error(`Failed to parse YAML variable content at path: ${path}`);
                        throw error;
                    }
                }
                if (o["is_secret"]) {
                    continue;
                }
            }
            catch (e) {
                log.warn(`Error reading variable ${path} to check for secrets`);
            }
        }
        // Handle branch-specific path mapping after all filtering
        if (specificItems) {
            const currentBranch = getCurrentGitBranch();
            if (currentBranch && isCurrentBranchFile(path)) {
                // This is a branch-specific file for current branch
                const basePath = fromBranchSpecificPath(path, currentBranch);
                if (isSpecificItem(basePath, specificItems)) {
                    // Map to base path for push operations
                    map[basePath] = content;
                    processedBasePaths.add(basePath);
                }
                else {
                    // Branch-specific file doesn't match pattern, skip it
                    continue;
                }
            }
            else if (!isBranchSpecificFile(path)) {
                // This is a regular base file, check if we should skip it
                if (processedBasePaths.has(path)) {
                    // Skip base file, we already processed branch-specific version
                    continue;
                }
                map[path] = content;
            }
        }
        else {
            // No specific items configuration, use regular path
            map[entry.path] = content;
        }
    }
    return map;
}
async function compareDynFSElement(els1, els2, ignore, json, skips, ignoreMetadataDeletion, codebases, ignoreCodebaseChanges, specificItems) {
    const [m1, m2] = els2
        ? await Promise.all([
            elementsToMap(els1, ignore, json, skips, specificItems),
            elementsToMap(els2, ignore, json, skips, specificItems),
        ])
        : [await elementsToMap(els1, ignore, json, skips, specificItems), {}];
    const changes = [];
    function parseYaml(k, v) {
        if (k.endsWith(".script.yaml")) {
            let o;
            try {
                o = yamlParseContent(k, v);
            }
            catch (error) {
                log.error(`Failed to parse script YAML content at path: ${k}`);
                throw error;
            }
            if (typeof o == "object") {
                if (Array.isArray(o?.["lock"])) {
                    o["lock"] = o["lock"].join("\n");
                }
                if (o["is_template"] != undefined) {
                    delete o["is_template"];
                }
            }
            return o;
        }
        else if (k.endsWith(".app.yaml")) {
            let o;
            try {
                o = yamlParseContent(k, v);
            }
            catch (error) {
                log.error(`Failed to parse app YAML content at path: ${k}`);
                throw error;
            }
            const o2 = o["policy"];
            if (typeof o2 == "object") {
                if (o2["on_behalf_of"] != undefined) {
                    delete o2["on_behalf_of"];
                }
                if (o2["on_behalf_of_email"] != undefined) {
                    delete o2["on_behalf_of_email"];
                }
            }
            return o;
        }
        else {
            try {
                return yamlParseContent(k, v);
            }
            catch (error) {
                log.error(`Failed to parse YAML content at path: ${k}`);
                throw error;
            }
        }
    }
    const codebaseChanges = {};
    for (let [k, v] of Object.entries(m1)) {
        const isScriptMetadata = k.endsWith(".script.yaml") || k.endsWith(".script.json");
        const skipMetadata = skips.skipScriptsMetadata && isScriptMetadata;
        if (m2[k] === undefined) {
            if (skipMetadata) {
                continue;
            }
            changes.push({ name: "added", path: k, content: v });
        }
        else {
            if (m2[k] == v) {
                continue;
            }
            else if (k.endsWith(".json")) {
                let parsedV, parsedM2;
                try {
                    parsedV = JSON.parse(v);
                }
                catch (error) {
                    log.error(`Failed to parse new JSON content for comparison at path: ${k}`);
                    throw error;
                }
                try {
                    parsedM2 = JSON.parse(m2[k]);
                }
                catch (error) {
                    log.error(`Failed to parse existing JSON content for comparison at path: ${k}`);
                    throw error;
                }
                if (deepEqual(parsedV, parsedM2)) {
                    continue;
                }
            }
            else if (k.endsWith(".yaml")) {
                const before = parseYaml(k, m2[k]);
                const after = parseYaml(k, v);
                if (deepEqual(before, after)) {
                    continue;
                }
                if (!ignoreCodebaseChanges) {
                    if (before?.codebase != undefined) {
                        delete before.codebase;
                        m2[k] = yamlStringify(before, yamlOptions);
                    }
                    if (after?.codebase != undefined) {
                        if (before.codebase != after.codebase) {
                            codebaseChanges[k] = after.codebase;
                        }
                        delete after.codebase;
                        v = yamlStringify(after, yamlOptions);
                    }
                }
                if (skipMetadata) {
                    continue;
                }
            }
            changes.push({
                name: "edited",
                path: k,
                after: v,
                before: m2[k],
                codebase: codebaseChanges[k],
            });
        }
    }
    const remoteCodebase = {};
    for (const [k] of Object.entries(m2)) {
        if (m1[k] === undefined) {
            if (!ignoreMetadataDeletion ||
                (!k?.endsWith(".script.yaml") && !k?.endsWith(".script.json"))) {
                changes.push({ name: "deleted", path: k });
            }
            else if (k?.endsWith(".script.yaml")) {
                let o = parseYaml(k, m2[k]);
                if (o.codebase != undefined) {
                    remoteCodebase[k] = o.codebase;
                }
            }
        }
    }
    if (!ignoreCodebaseChanges) {
        for (const [k, v] of Object.entries(remoteCodebase)) {
            const tsFile = k.replace(".script.yaml", ".ts");
            if (changes.find((c) => c.path == tsFile && (c.name == "edited" || c.name == "deleted"))) {
                continue;
            }
            const c = findCodebase(tsFile, codebases);
            if ((await c?.getDigest()) != v) {
                changes.push({
                    name: "edited",
                    path: tsFile,
                    codebase: v,
                    before: m1[tsFile],
                    after: m2[tsFile],
                });
            }
        }
    }
    if (!ignoreCodebaseChanges) {
        for (const change of changes) {
            const codebase = codebaseChanges[change.path];
            if (!codebase)
                continue;
            const tsFile = change.path.replace(".script.yaml", ".ts");
            if (change.name == "edited" && change.path == tsFile) {
                change.codebase = codebase;
            }
        }
    }
    changes.sort((a, b) => getOrderFromPath(a.path) == getOrderFromPath(b.path)
        ? a.path.localeCompare(b.path)
        : getOrderFromPath(a.path) - getOrderFromPath(b.path));
    return changes;
}
function getOrderFromPath(p) {
    const typ = getTypeStrFromPath(p);
    if (typ == "settings") {
        return 0;
    }
    else if (typ == "folder") {
        return 1;
    }
    else if (typ == "resource-type") {
        return 2;
    }
    else if (typ == "resource") {
        return 3;
    }
    else if (typ == "script") {
        return 4;
    }
    else if (typ == "flow") {
        return 5;
    }
    else if (typ == "app") {
        return 6;
    }
    else if (typ == "schedule") {
        return 7;
    }
    else if (typ == "http_trigger" ||
        typ == "websocket_trigger" ||
        typ == "kafka_trigger" ||
        typ == "nats_trigger" ||
        typ == "postgres_trigger" ||
        typ == "mqtt_trigger" ||
        typ == "sqs_trigger" ||
        typ == "gcp_trigger" ||
        typ == "email_trigger") {
        return 8;
    }
    else if (typ == "variable") {
        return 9;
    }
    else if (typ == "user") {
        return 10;
    }
    else if (typ == "group") {
        return 11;
    }
    else if (typ == "encryption_key") {
        return 12;
    }
    else {
        return 13;
    }
}
const isNotWmillFile = (p, isDirectory) => {
    if (p.endsWith(SEP)) {
        return false;
    }
    if (isDirectory) {
        return (!p.startsWith("u" + SEP) &&
            !p.startsWith("f" + SEP) &&
            !p.startsWith("g" + SEP) &&
            !p.startsWith("users" + SEP) &&
            !p.startsWith("groups" + SEP));
    }
    try {
        const typ = getTypeStrFromPath(p);
        if (typ == "resource-type" ||
            typ == "settings" ||
            typ == "encryption_key") {
            return p.includes(SEP);
        }
        else {
            return (!p.startsWith("u" + SEP) &&
                !p.startsWith("f" + SEP) &&
                !p.startsWith("g" + SEP) &&
                !p.startsWith("users" + SEP) &&
                !p.startsWith("groups" + SEP));
        }
    }
    catch {
        return true;
    }
};
export const isWhitelisted = (p) => {
    return (p == "." + SEP ||
        p == "" ||
        p == "u" ||
        p == "f" ||
        p == "g" ||
        p == "users" ||
        p == "groups");
};
export async function ignoreF(wmillconf) {
    let whitelist = undefined;
    if ((Array.isArray(wmillconf?.includes) && wmillconf?.includes?.length > 0) ||
        (Array.isArray(wmillconf?.excludes) && wmillconf?.excludes?.length > 0)) {
        whitelist = {
            approve(file) {
                return ((!wmillconf.includes ||
                    wmillconf.includes?.some((i) => minimatch(file, i))) &&
                    (!wmillconf?.excludes ||
                        wmillconf.excludes.every((i) => !minimatch(file, i))) &&
                    (!wmillconf.extraIncludes ||
                        wmillconf.extraIncludes.length === 0 ||
                        wmillconf.extraIncludes.some((i) => minimatch(file, i))));
            },
        };
    }
    try {
        await dntShim.Deno.stat(".wmillignore");
        throw Error(".wmillignore is not supported anymore, switch to wmill.yaml");
    }
    catch {
        //expected
    }
    // new Gitignore.default({ initialRules: ignoreContent.split("\n")}).ignoreContent).compile();
    return (p, isDirectory) => {
        const ext = wmillconf.json ? ".json" : ".yaml";
        if (!isDirectory && p.endsWith(".resource-type" + ext)) {
            return wmillconf.skipResourceTypes ?? false;
        }
        // Special files should bypass path-based filtering when their include flags are set
        if (!isDirectory) {
            try {
                const fileType = getTypeStrFromPath(p);
                if (wmillconf.includeUsers && fileType === "user") {
                    return false; // Don't ignore, always include
                }
                if (wmillconf.includeGroups && fileType === "group") {
                    return false; // Don't ignore, always include
                }
                if (wmillconf.includeSettings && fileType === "settings") {
                    return false; // Don't ignore, always include
                }
                if (wmillconf.includeKey && fileType === "encryption_key") {
                    return false; // Don't ignore, always include
                }
            }
            catch {
                // If getTypeStrFromPath can't determine the type, fall through to normal logic
            }
        }
        return (!isWhitelisted(p) &&
            (isNotWmillFile(p, isDirectory) ||
                (!isDirectory && whitelist != undefined && !whitelist.approve(p))));
    };
}
// deno-lint-ignore no-inner-declarations
async function addToChangedIfNotExists(p, tracker) {
    const isScript = exts.some((e) => p.endsWith(e));
    if (isScript) {
        if (p.includes(".flow" + SEP)) {
            const folder = p.substring(0, p.indexOf(".flow" + SEP)) + ".flow" + SEP;
            if (!tracker.flows.includes(folder)) {
                tracker.flows.push(folder);
            }
        }
        else if (p.includes(".app" + SEP)) {
            const folder = p.substring(0, p.indexOf(".app" + SEP)) + ".app" + SEP;
            if (!tracker.apps.includes(folder)) {
                tracker.apps.push(folder);
            }
        }
        else {
            if (!tracker.scripts.includes(p)) {
                tracker.scripts.push(p);
            }
        }
    }
    else if (p.endsWith(".script.yaml") || p.endsWith(".script.json")) {
        try {
            const contentPath = await findContentFile(p);
            if (!contentPath)
                return;
            if (tracker.scripts.includes(contentPath))
                return;
            tracker.scripts.push(contentPath);
        }
        catch {
            // ignore
        }
    }
}
async function buildTracker(changes) {
    const tracker = {
        scripts: [],
        flows: [],
        apps: [],
    };
    for (const change of changes) {
        if (change.name == "added" || change.name == "edited") {
            await addToChangedIfNotExists(change.path, tracker);
        }
    }
    return tracker;
}
export async function pull(opts) {
    const originalCliOpts = { ...opts };
    opts = await mergeConfigWithConfigFile(opts);
    // Validate branch configuration early
    try {
        await validateBranchConfiguration(opts);
    }
    catch (error) {
        if (error instanceof Error && error.message.includes("overrides")) {
            log.error(error.message);
            dntShim.Deno.exit(1);
        }
        throw error;
    }
    if (opts.stateful) {
        await ensureDir(path.join(dntShim.Deno.cwd(), ".wmill"));
    }
    const workspace = await resolveWorkspace(opts);
    await requireLogin(opts);
    // Resolve effective sync options with branch awareness
    const effectiveOpts = await resolveEffectiveSyncOptions(workspace, opts, opts.promotion);
    // Extract specific items configuration before merging overwrites gitBranches
    const specificItems = getSpecificItemsForCurrentBranch(opts);
    // Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
    opts = mergeCliWithEffectiveOptions(originalCliOpts, effectiveOpts);
    const codebases = await listSyncCodebases(opts);
    log.info(colors.gray("Computing the files to update locally to match remote (taking wmill.yaml into account)"));
    let resourceTypeToFormatExtension = {};
    try {
        resourceTypeToFormatExtension = (await wmill.fileResourceTypeToFileExtMap({
            workspace: workspace.workspaceId,
        }));
    }
    catch {
        // ignore
    }
    const remote = ZipFSElement((await downloadZip(workspace, opts.plainSecrets, opts.skipVariables, opts.skipResources, opts.skipResourceTypes, opts.skipSecrets, opts.includeSchedules, opts.includeTriggers, opts.includeUsers, opts.includeGroups, opts.includeSettings, opts.includeKey, opts.defaultTs)), !opts.json, opts.defaultTs ?? "bun", resourceTypeToFormatExtension, true);
    const local = !opts.stateful
        ? await FSFSElement(dntShim.Deno.cwd(), codebases, true)
        : await FSFSElement(path.join(dntShim.Deno.cwd(), ".wmill"), [], true);
    const changes = await compareDynFSElement(remote, local, await ignoreF(opts), opts.json ?? false, opts, false, codebases, true, specificItems);
    log.info(`remote (${workspace.name}) -> local: ${changes.length} changes to apply`);
    // Handle JSON output for dry-run
    if (opts.dryRun && opts.jsonOutput) {
        const result = {
            success: true,
            changes: changes.map((change) => ({
                type: change.name,
                path: change.path,
                ...(change.name === "edited" && change.codebase
                    ? { codebase_changed: true }
                    : {}),
                ...(specificItems && isSpecificItem(change.path, specificItems)
                    ? {
                        branch_specific: true,
                        branch_specific_path: getBranchSpecificPath(change.path, specificItems),
                    }
                    : {}),
            })),
            total: changes.length,
        };
        console.log(JSON.stringify(result, null, 2));
        return;
    }
    if (changes.length > 0) {
        if (!opts.jsonOutput) {
            prettyChanges(changes, specificItems);
        }
        if (opts.dryRun) {
            log.info(colors.gray(`Dry run complete.`));
            return;
        }
        if (!opts.yes &&
            !(await Confirm.prompt({
                message: `Do you want to apply these ${changes.length} changes to your local files?`,
                default: true,
            }))) {
            return;
        }
        const conflicts = [];
        log.info(colors.gray(`Applying changes to files ...`));
        for await (const change of changes) {
            // Determine if this file should be written to a branch-specific path
            let targetPath = change.path;
            if (specificItems && isSpecificItem(change.path, specificItems)) {
                const branchSpecificPath = getBranchSpecificPath(change.path, specificItems);
                if (branchSpecificPath) {
                    targetPath = branchSpecificPath;
                }
            }
            const target = path.join(dntShim.Deno.cwd(), targetPath);
            const stateTarget = path.join(dntShim.Deno.cwd(), ".wmill", targetPath);
            if (change.name === "edited") {
                if (opts.stateful) {
                    try {
                        const currentLocal = await dntShim.Deno.readTextFile(target);
                        if (currentLocal !== change.before &&
                            currentLocal !== change.after) {
                            log.info(colors.red(`Conflict detected on ${change.path}\nBoth local and remote have been modified.`));
                            if (opts.failConflicts) {
                                conflicts.push({
                                    local: currentLocal,
                                    change,
                                    path: change.path,
                                });
                                continue;
                            }
                            else if (opts.yes) {
                                log.info(colors.red(`Override local version with remote since --yes was passed and no --fail-conflicts.`));
                            }
                            else {
                                showConflict(change.path, currentLocal, change.after);
                                if (await Confirm.prompt("Preserve local (push to change remote and avoid seeing this again)?")) {
                                    continue;
                                }
                            }
                        }
                    }
                    catch {
                        // ignore
                    }
                }
                if (exts.some((e) => change.path.endsWith(e))) {
                    log.info(`Editing script content of ${targetPath}${targetPath !== change.path
                        ? colors.gray(` (branch-specific override for ${change.path})`)
                        : ""}`);
                }
                else if (change.path.endsWith(".yaml") ||
                    change.path.endsWith(".json")) {
                    log.info(`Editing ${getTypeStrFromPath(change.path)} ${targetPath}${targetPath !== change.path
                        ? colors.gray(` (branch-specific override for ${change.path})`)
                        : ""}`);
                }
                await dntShim.Deno.writeTextFile(target, change.after);
                if (opts.stateful) {
                    await ensureDir(path.dirname(stateTarget));
                    await dntShim.Deno.copyFile(target, stateTarget);
                }
            }
            else if (change.name === "added") {
                await ensureDir(path.dirname(target));
                if (opts.stateful) {
                    await ensureDir(path.dirname(stateTarget));
                    log.info(`Adding ${getTypeStrFromPath(change.path)} ${targetPath}${targetPath !== change.path
                        ? colors.gray(` (branch-specific override for ${change.path})`)
                        : ""}`);
                }
                await dntShim.Deno.writeTextFile(target, change.content);
                log.info(`Writing ${getTypeStrFromPath(change.path)} ${targetPath}${targetPath !== change.path
                    ? colors.gray(` (branch-specific override for ${change.path})`)
                    : ""}`);
                if (opts.stateful) {
                    await dntShim.Deno.copyFile(target, stateTarget);
                }
            }
            else if (change.name === "deleted") {
                try {
                    log.info(`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`);
                    await dntShim.Deno.remove(target);
                    if (opts.stateful) {
                        await dntShim.Deno.remove(stateTarget);
                    }
                }
                catch {
                    if (opts.stateful) {
                        await dntShim.Deno.remove(stateTarget);
                    }
                }
            }
        }
        if (opts.failConflicts) {
            if (conflicts.length > 0) {
                console.error(colors.red(`Conflicts were found`));
                log.info("Conflicts:");
                for (const conflict of conflicts) {
                    showConflict(conflict.path, conflict.local, conflict.change.after);
                }
                log.info(colors.red(`Please resolve these conflicts manually by either:
  - reverting the content back to its remote (\`wmill pull\` and refuse to preserve local when prompted)
  - pushing the changes with \`wmill push --skip-pull\` to override wmill with all your local changes
`));
                dntShim.Deno.exit(1);
            }
        }
        log.info("All local changes pulled, now updating wmill-lock.yaml");
        await readLockfile(); // ensure wmill-lock.yaml exists
        const globalDeps = await findGlobalDeps();
        const tracker = await buildTracker(changes);
        for (const change of tracker.scripts) {
            await generateScriptMetadataInternal(change, workspace, opts, false, true, globalDeps, codebases, true);
        }
        for (const change of tracker.flows) {
            log.info(`Updating lock for flow ${change}`);
            await generateFlowLockInternal(change, false, workspace, opts, true);
        }
        if (tracker.apps.length > 0) {
            log.info(`Apps ${tracker.apps.join(", ")} scripts were changed but ignoring for now`);
        }
        if (opts.jsonOutput) {
            const result = {
                success: true,
                message: `All ${changes.length} changes applied locally and wmill-lock.yaml updated`,
                changes: changes.map((change) => ({
                    type: change.name,
                    path: change.path,
                    ...(change.name === "edited" && change.codebase
                        ? { codebase_changed: true }
                        : {}),
                    ...(specificItems && isSpecificItem(change.path, specificItems)
                        ? {
                            branch_specific: true,
                            branch_specific_path: getBranchSpecificPath(change.path, specificItems),
                        }
                        : {}),
                })),
                total: changes.length,
            };
            console.log(JSON.stringify(result, null, 2));
        }
        else {
            log.info(colors.bold.green.underline(`\nDone! All ${changes.length} changes applied locally and wmill-lock.yaml updated.`));
        }
    }
    else if (opts.jsonOutput) {
        console.log(JSON.stringify({ success: true, message: "No changes to apply", total: 0 }, null, 2));
    }
}
function prettyChanges(changes, specificItems) {
    for (const change of changes) {
        let displayPath = change.path;
        let branchNote = "";
        // Check if this will be written as a branch-specific file
        if (specificItems && isSpecificItem(change.path, specificItems)) {
            const branchSpecificPath = getBranchSpecificPath(change.path, specificItems);
            if (branchSpecificPath) {
                displayPath = branchSpecificPath;
                branchNote = " (branch-specific)";
            }
        }
        if (change.name === "added") {
            log.info(colors.green(`+ ${getTypeStrFromPath(change.path)} ` +
                displayPath +
                colors.gray(branchNote)));
        }
        else if (change.name === "deleted") {
            log.info(colors.red(`- ${getTypeStrFromPath(change.path)} ` +
                displayPath +
                colors.gray(branchNote)));
        }
        else if (change.name === "edited") {
            log.info(colors.yellow(`~ ${getTypeStrFromPath(change.path)} ` +
                displayPath +
                colors.gray(branchNote) +
                (change.codebase ? ` (codebase changed)` : "")));
            if (change.before != change.after) {
                if (change.path.endsWith(".yaml")) {
                    try {
                        showDiff(yamlStringify(yamlParseContent(change.path, change.before), yamlOptions), yamlStringify(yamlParseContent(change.path, change.after), yamlOptions));
                    }
                    catch {
                        showDiff(change.before, change.after);
                    }
                }
                else {
                    showDiff(change.before, change.after);
                }
            }
        }
    }
}
// function prettyDiff(diffs: Difference[]) {
//   for (const diff of diffs) {
//     let pathString = "";
//     for (const pathSegment of diff.path) {
//       if (typeof pathSegment === "string") {
//         pathString += ".";
//         pathString += pathSegment;
//       } else {
//         pathString += "[";
//         pathString += pathSegment;
//         pathString += "]";
//       }
//     }
//     if (diff.type === "REMOVE" || diff.type === "CHANGE") {
//      log.info(colors.red("- " + pathString + " = " + diff.oldValue));
//     }
//     if (diff.type === "CREATE" || diff.type === "CHANGE") {
//      log.info(colors.green("+ " + pathString + " = " + diff.value));
//     }
//   }
// }
function removeSuffix(str, suffix) {
    return str.slice(0, str.length - suffix.length);
}
export async function push(opts) {
    // Save original CLI options before merging with config file
    const originalCliOpts = { ...opts };
    // Load configuration from wmill.yaml and merge with CLI options
    opts = await mergeConfigWithConfigFile(opts);
    // Validate branch configuration early
    try {
        await validateBranchConfiguration(opts);
    }
    catch (error) {
        if (error instanceof Error && error.message.includes("overrides")) {
            log.error(error.message);
            dntShim.Deno.exit(1);
        }
        throw error;
    }
    const workspace = await resolveWorkspace(opts);
    await requireLogin(opts);
    // Resolve effective sync options with branch awareness
    const effectiveOpts = await resolveEffectiveSyncOptions(workspace, opts, opts.promotion);
    // Extract specific items configuration BEFORE merging overwrites gitBranches
    const specificItems = getSpecificItemsForCurrentBranch(opts);
    // Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
    opts = mergeCliWithEffectiveOptions(originalCliOpts, effectiveOpts);
    const codebases = await listSyncCodebases(opts);
    if (opts.raw) {
        log.info("--raw is now the default, you can remove it as a flag");
    }
    if (opts.stateful) {
        if (!opts.skipPull) {
            log.info(colors.gray("You need to be up-to-date before pushing, pulling first."));
            await pull(opts);
            log.info(colors.green("Pull done, now pushing."));
            log.info("\n");
        }
    }
    log.info(colors.gray("Computing the files to update on the remote to match local (taking wmill.yaml includes/excludes into account)"));
    let resourceTypeToFormatExtension = {};
    try {
        resourceTypeToFormatExtension = (await wmill.fileResourceTypeToFileExtMap({
            workspace: workspace.workspaceId,
        }));
    }
    catch {
        // ignore
    }
    const remote = ZipFSElement((await downloadZip(workspace, opts.plainSecrets, opts.skipVariables, opts.skipResources, opts.skipResourceTypes, opts.skipSecrets, opts.includeSchedules, opts.includeTriggers, opts.includeUsers, opts.includeGroups, opts.includeSettings, opts.includeKey, opts.defaultTs)), !opts.json, opts.defaultTs ?? "bun", resourceTypeToFormatExtension, false);
    const local = await FSFSElement(path.join(dntShim.Deno.cwd(), ""), codebases, false);
    const changes = await compareDynFSElement(local, remote, await ignoreF(opts), opts.json ?? false, opts, true, codebases, false, specificItems);
    const globalDeps = await findGlobalDeps();
    const tracker = await buildTracker(changes);
    const staleScripts = [];
    const staleFlows = [];
    for (const change of tracker.scripts) {
        const stale = await generateScriptMetadataInternal(change, workspace, opts, true, true, globalDeps, codebases, false);
        if (stale) {
            staleScripts.push(stale);
        }
    }
    if (staleScripts.length > 0) {
        log.info("");
        log.warn("Stale scripts metadata found, you may want to update them using 'wmill script generate-metadata' before pushing:");
        for (const stale of staleScripts) {
            log.warn(stale);
        }
        log.info("");
    }
    for (const change of tracker.flows) {
        const stale = await generateFlowLockInternal(change, true, workspace, opts, false, true);
        if (stale) {
            staleFlows.push(stale);
        }
    }
    if (staleFlows.length > 0) {
        log.warn("Stale flows locks found, you may want to update them using 'wmill flow generate-locks' before pushing:");
        for (const stale of staleFlows) {
            log.warn(stale);
        }
        log.info("");
    }
    const version = await fetchVersion(workspace.remote);
    log.info(colors.gray("Remote version: " + version));
    log.info(`remote (${workspace.name}) <- local: ${changes.length} changes to apply`);
    // Handle JSON output for dry-run
    if (opts.dryRun && opts.jsonOutput) {
        const result = {
            success: true,
            changes: changes.map((change) => ({
                type: change.name,
                path: change.path,
                ...(change.name === "edited" && change.codebase
                    ? { codebase_changed: true }
                    : {}),
                ...(specificItems && isSpecificItem(change.path, specificItems)
                    ? {
                        branch_specific: true,
                        branch_specific_path: getBranchSpecificPath(change.path, specificItems),
                    }
                    : {}),
            })),
            total: changes.length,
        };
        console.log(JSON.stringify(result, null, 2));
        return;
    }
    if (changes.length > 0) {
        if (!opts.jsonOutput) {
            prettyChanges(changes, specificItems);
        }
        if (opts.dryRun) {
            log.info(colors.gray(`Dry run complete.`));
            return;
        }
        if (!opts.yes &&
            !(await Confirm.prompt({
                message: `Do you want to apply these ${changes.length} changes to the remote?`,
                default: true,
            }))) {
            return;
        }
        const start = performance.now();
        log.info(colors.gray(`Applying changes to files ...`));
        let stateful = opts.stateful;
        if (stateful) {
            try {
                await dntShim.Deno.stat(path.join(dntShim.Deno.cwd(), ".wmill"));
            }
            catch {
                stateful = false;
            }
        }
        // Group changes by base path (before first dot)
        const groupedChanges = new Map();
        for (const change of changes) {
            const basePath = change.path.split(".")[0];
            if (!groupedChanges.has(basePath)) {
                groupedChanges.set(basePath, []);
            }
            groupedChanges.get(basePath).push(change);
        }
        let parallelizationFactor = opts.parallel ?? 1;
        if (parallelizationFactor <= 0) {
            parallelizationFactor = 1;
        }
        const groupedChangesArray = Array.from(groupedChanges.entries());
        log.info(`found changes for ${groupedChangesArray.length} items with a total of ${groupedChangesArray.reduce((acc, [_, changes]) => acc + changes.length, 0)} files to process`);
        if (parallelizationFactor > 1) {
            log.info(`Parallelizing ${parallelizationFactor} changes at a time`);
        }
        // Create a pool of workers that processes items as they become available
        const pool = new Set();
        const queue = [...groupedChangesArray];
        while (queue.length > 0 || pool.size > 0) {
            // Fill the pool until we reach parallelizationFactor
            while (pool.size < parallelizationFactor && queue.length > 0) {
                const [_basePath, changes] = queue.shift();
                const promise = (async () => {
                    const alreadySynced = [];
                    for await (const change of changes) {
                        let stateTarget = undefined;
                        if (stateful) {
                            try {
                                stateTarget = path.join(dntShim.Deno.cwd(), ".wmill", change.path);
                                await dntShim.Deno.stat(stateTarget);
                            }
                            catch {
                                stateTarget = undefined;
                            }
                        }
                        if (change.name === "edited") {
                            if (await handleScriptMetadata(change.path, workspace, alreadySynced, opts.message, globalDeps, codebases, opts)) {
                                if (stateTarget) {
                                    await dntShim.Deno.writeTextFile(stateTarget, change.after);
                                }
                                continue;
                            }
                            else if (await handleFile(change.path, workspace, alreadySynced, opts.message, opts, globalDeps, codebases)) {
                                if (stateTarget) {
                                    await dntShim.Deno.writeTextFile(stateTarget, change.after);
                                }
                                continue;
                            }
                            if (stateTarget) {
                                await ensureDir(path.dirname(stateTarget));
                                log.info(`Editing ${getTypeStrFromPath(change.path)} ${change.path}`);
                            }
                            if (isFileResource(change.path)) {
                                const resourceFilePath = await findResourceFile(change.path);
                                if (!alreadySynced.includes(resourceFilePath)) {
                                    alreadySynced.push(resourceFilePath);
                                    const newObj = parseFromPath(resourceFilePath, await dntShim.Deno.readTextFile(resourceFilePath));
                                    // For branch-specific resources, push to the base path on the workspace server
                                    // This ensures branch-specific files are stored with their base names in the workspace
                                    let serverPath = resourceFilePath;
                                    const currentBranch = getCurrentGitBranch();
                                    if (currentBranch && isBranchSpecificFile(resourceFilePath)) {
                                        serverPath = fromBranchSpecificPath(resourceFilePath, currentBranch);
                                    }
                                    await pushResource(workspace.workspaceId, serverPath, undefined, newObj, resourceFilePath);
                                    if (stateTarget) {
                                        await dntShim.Deno.writeTextFile(stateTarget, change.after);
                                    }
                                    continue;
                                }
                            }
                            const oldObj = parseFromPath(change.path, change.before);
                            const newObj = parseFromPath(change.path, change.after);
                            // Check if this is a branch-specific item and get the original branch-specific path
                            let originalBranchSpecificPath;
                            if (specificItems && isSpecificItem(change.path, specificItems)) {
                                originalBranchSpecificPath = getBranchSpecificPath(change.path, specificItems);
                            }
                            await pushObj(workspace.workspaceId, change.path, oldObj, newObj, opts.plainSecrets ?? false, alreadySynced, opts.message, originalBranchSpecificPath);
                            if (stateTarget) {
                                await dntShim.Deno.writeTextFile(stateTarget, change.after);
                            }
                        }
                        else if (change.name === "added") {
                            if (change.path.endsWith(".script.json") ||
                                change.path.endsWith(".script.yaml") ||
                                change.path.endsWith(".lock") ||
                                isFileResource(change.path)) {
                                continue;
                            }
                            else if (await handleFile(change.path, workspace, alreadySynced, opts.message, opts, globalDeps, codebases)) {
                                continue;
                            }
                            if (stateTarget) {
                                await ensureDir(path.dirname(stateTarget));
                                log.info(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`);
                            }
                            const obj = parseFromPath(change.path, change.content);
                            // Determine the actual local file path for this change
                            // For branch-specific items, we read from branch-specific files but push to base server paths
                            let localFilePath = change.path;
                            if (specificItems && isSpecificItem(change.path, specificItems)) {
                                const branchSpecificPath = getBranchSpecificPath(change.path, specificItems);
                                if (branchSpecificPath) {
                                    localFilePath = branchSpecificPath;
                                }
                            }
                            await pushObj(workspace.workspaceId, change.path, undefined, obj, opts.plainSecrets ?? false, [], opts.message, localFilePath // Pass the actual local file path
                            );
                            if (stateTarget) {
                                await dntShim.Deno.writeTextFile(stateTarget, change.content);
                            }
                        }
                        else if (change.name === "deleted") {
                            if (change.path.endsWith(".lock")) {
                                continue;
                            }
                            const typ = getTypeStrFromPath(change.path);
                            if (typ == "script") {
                                log.info(`Archiving ${typ} ${change.path}`);
                            }
                            else {
                                log.info(`Deleting ${typ} ${change.path}`);
                            }
                            const workspaceId = workspace.workspaceId;
                            const target = change.path.replaceAll(SEP, "/");
                            switch (typ) {
                                case "script": {
                                    await wmill.archiveScriptByPath({
                                        workspace: workspaceId,
                                        path: removeExtensionToPath(target),
                                    });
                                    break;
                                }
                                case "folder":
                                    await wmill.deleteFolder({
                                        workspace: workspaceId,
                                        name: change.path.split(SEP)[1],
                                    });
                                    break;
                                case "resource":
                                    await wmill.deleteResource({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".resource.json"),
                                    });
                                    break;
                                case "resource-type":
                                    await wmill.deleteResourceType({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".resource-type.json"),
                                    });
                                    break;
                                case "flow":
                                    await wmill.deleteFlowByPath({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".flow/flow.json"),
                                    });
                                    break;
                                case "app":
                                    await wmill.deleteApp({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".app/app.json"),
                                    });
                                    break;
                                case "schedule":
                                    await wmill.deleteSchedule({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".schedule.json"),
                                    });
                                    break;
                                case "http_trigger":
                                    await wmill.deleteHttpTrigger({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".http_trigger.json"),
                                    });
                                    break;
                                case "websocket_trigger":
                                    await wmill.deleteWebsocketTrigger({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".websocket_trigger.json"),
                                    });
                                    break;
                                case "kafka_trigger":
                                    await wmill.deleteKafkaTrigger({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".kafka_trigger.json"),
                                    });
                                    break;
                                case "nats_trigger":
                                    await wmill.deleteNatsTrigger({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".nats_trigger.json"),
                                    });
                                    break;
                                case "postgres_trigger":
                                    await wmill.deletePostgresTrigger({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".postgres_trigger.json"),
                                    });
                                    break;
                                case "mqtt_trigger":
                                    await wmill.deleteMqttTrigger({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".mqtt_trigger.json"),
                                    });
                                    break;
                                case "sqs_trigger":
                                    await wmill.deleteSqsTrigger({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".sqs_trigger.json"),
                                    });
                                    break;
                                case "gcp_trigger":
                                    await wmill.deleteGcpTrigger({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".gcp_trigger.json"),
                                    });
                                    break;
                                case "email_trigger":
                                    await wmill.deleteEmailTrigger({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".email_trigger.json"),
                                    });
                                    break;
                                case "variable":
                                    await wmill.deleteVariable({
                                        workspace: workspaceId,
                                        path: removeSuffix(target, ".variable.json"),
                                    });
                                    break;
                                case "user": {
                                    const users = await wmill.listUsers({
                                        workspace: workspaceId,
                                    });
                                    const email = removeSuffix(removePathPrefix(change.path, "users"), ".user.json");
                                    const user = users.find((u) => u.email === email);
                                    if (!user) {
                                        throw new Error(`User ${email} not found`);
                                    }
                                    await wmill.deleteUser({
                                        workspace: workspaceId,
                                        username: user.username,
                                    });
                                    break;
                                }
                                case "group":
                                    await wmill.deleteGroup({
                                        workspace: workspaceId,
                                        name: removeSuffix(removePathPrefix(change.path, "groups"), ".group.json"),
                                    });
                                    break;
                                default:
                                    break;
                            }
                            if (stateTarget) {
                                try {
                                    await dntShim.Deno.remove(stateTarget);
                                }
                                catch {
                                    // state target may not exist already
                                }
                            }
                        }
                    }
                })();
                pool.add(promise);
                // Remove from pool when complete
                promise.then(() => pool.delete(promise));
            }
            // Wait for at least one task to complete before continuing
            if (pool.size > 0) {
                await Promise.race(pool);
            }
        }
        if (opts.jsonOutput) {
            const result = {
                success: true,
                message: `All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name}`,
                changes: changes.map((change) => ({
                    type: change.name,
                    path: change.path,
                    ...(change.name === "edited" && change.codebase
                        ? { codebase_changed: true }
                        : {}),
                    ...(specificItems && isSpecificItem(change.path, specificItems)
                        ? {
                            branch_specific: true,
                            branch_specific_path: getBranchSpecificPath(change.path, specificItems),
                        }
                        : {}),
                })),
                total: changes.length,
                duration_ms: Math.round(performance.now() - start),
            };
            console.log(JSON.stringify(result, null, 2));
        }
        else {
            log.info(colors.bold.green.underline(`\nDone! All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name} (${(performance.now() - start).toFixed(0)}ms)`));
        }
    }
    else if (opts.jsonOutput) {
        console.log(JSON.stringify({ success: true, message: "No changes to push", total: 0 }, null, 2));
    }
}
const command = new Command()
    .description("sync local with a remote workspaces or the opposite (push or pull)")
    .action(() => log.info("2 actions available, pull and push. Use -h to display help."))
    .command("pull")
    .description("Pull any remote changes and apply them locally.")
    .option("--yes", "Pull without needing confirmation")
    .option("--dry-run", "Show changes that would be pulled without actually pushing")
    .option("--plain-secrets", "Pull secrets as plain text")
    .option("--json", "Use JSON instead of YAML")
    .option("--skip-variables", "Skip syncing variables (including secrets)")
    .option("--skip-secrets", "Skip syncing only secrets variables")
    .option("--skip-resources", "Skip syncing  resources")
    .option("--skip-resource-types", "Skip syncing  resource types")
    .option("--skip-scripts", "Skip syncing scripts")
    .option("--skip-flows", "Skip syncing flows")
    .option("--skip-apps", "Skip syncing apps")
    .option("--skip-folders", "Skip syncing folders")
    // .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
    .option("--include-schedules", "Include syncing  schedules")
    .option("--include-triggers", "Include syncing triggers")
    .option("--include-users", "Include syncing users")
    .option("--include-groups", "Include syncing groups")
    .option("--include-settings", "Include syncing workspace settings")
    .option("--include-key", "Include workspace encryption key")
    .option("--skip-branch-validation", "Skip git branch validation and prompts")
    .option("--json-output", "Output results in JSON format")
    .option("-i --includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Overrides wmill.yaml includes")
    .option("-e --excludes <patterns:file[]>", "Comma separated patterns to specify which file to NOT take into account. Overrides wmill.yaml excludes")
    .option("--extra-includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Useful to still take wmill.yaml into account and act as a second pattern to satisfy")
    .option("--repository <repo:string>", "Specify repository path (e.g., u/user/repo) when multiple repositories exist")
    .option("--promotion <branch:string>", "Use promotionOverrides from the specified branch instead of regular overrides")
    // deno-lint-ignore no-explicit-any
    .action(pull)
    .command("push")
    .description("Push any local changes and apply them remotely.")
    .option("--yes", "Push without needing confirmation")
    .option("--dry-run", "Show changes that would be pushed without actually pushing")
    .option("--plain-secrets", "Push secrets as plain text")
    .option("--json", "Use JSON instead of YAML")
    .option("--skip-variables", "Skip syncing variables (including secrets)")
    .option("--skip-secrets", "Skip syncing only secrets variables")
    .option("--skip-resources", "Skip syncing  resources")
    .option("--skip-resource-types", "Skip syncing  resource types")
    .option("--skip-scripts", "Skip syncing scripts")
    .option("--skip-flows", "Skip syncing flows")
    .option("--skip-apps", "Skip syncing apps")
    .option("--skip-folders", "Skip syncing folders")
    // .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
    .option("--include-schedules", "Include syncing schedules")
    .option("--include-triggers", "Include syncing triggers")
    .option("--include-users", "Include syncing users")
    .option("--include-groups", "Include syncing groups")
    .option("--include-settings", "Include syncing workspace settings")
    .option("--include-key", "Include workspace encryption key")
    .option("--skip-branch-validation", "Skip git branch validation and prompts")
    .option("--json-output", "Output results in JSON format")
    .option("-i --includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)")
    .option("-e --excludes <patterns:file[]>", "Comma separated patterns to specify which file to NOT take into account.")
    .option("--extra-includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Useful to still take wmill.yaml into account and act as a second pattern to satisfy")
    .option("--message <message:string>", "Include a message that will be added to all scripts/flows/apps updated during this push")
    .option("--parallel <number>", "Number of changes to process in parallel")
    .option("--repository <repo:string>", "Specify repository path (e.g., u/user/repo) when multiple repositories exist")
    // deno-lint-ignore no-explicit-any
    .action(push);
export default command;
