function syncAssets(source_dir, target_dir)
    if not os.isdir(source_dir) then
        return
    end

    local copied = 0
    local skipped = 0
    local removed = 0
    local source_files = {}

    for _, source_file in ipairs(os.files(path.join(source_dir, "**"))) do
        local relative_path = path.relative(source_file, source_dir)
        local target_file = path.join(target_dir, relative_path)
        local target_subdir = path.directory(target_file)
        local should_copy = not os.isfile(target_file)

        source_files[relative_path] = true

        if not should_copy then
            local source_mtime = os.mtime(source_file)
            local target_mtime = os.mtime(target_file)
            local source_size = os.filesize(source_file)
            local target_size = os.filesize(target_file)
            should_copy = source_mtime > target_mtime or source_size ~= target_size
        end

        if should_copy then
            if target_subdir ~= "." and not os.isdir(target_subdir) then
                os.mkdir(target_subdir)
            end
            os.cp(source_file, target_file)
            copied = copied + 1
        else
            skipped = skipped + 1
        end
    end

    if os.isdir(target_dir) then
        for _, target_file in ipairs(os.files(path.join(target_dir, "**"))) do
            local relative_path = path.relative(target_file, target_dir)
            if not source_files[relative_path] then
                os.rm(target_file)
                removed = removed + 1
            end
        end
    end

    print(string.format("Sync assets: %d copied, %d skipped, %d removed", copied, skipped, removed))
end
