#
tokens: 53761/50000 1/497 files (page 20/20)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 20 of 20. Use http://codebase.md/trycua/cua?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .all-contributorsrc
├── .cursorignore
├── .devcontainer
│   ├── devcontainer.json
│   ├── post-install.sh
│   └── README.md
├── .dockerignore
├── .gitattributes
├── .github
│   ├── FUNDING.yml
│   ├── scripts
│   │   ├── get_pyproject_version.py
│   │   └── tests
│   │       ├── __init__.py
│   │       ├── README.md
│   │       └── test_get_pyproject_version.py
│   └── workflows
│       ├── ci-lume.yml
│       ├── docker-publish-kasm.yml
│       ├── docker-publish-xfce.yml
│       ├── docker-reusable-publish.yml
│       ├── npm-publish-computer.yml
│       ├── npm-publish-core.yml
│       ├── publish-lume.yml
│       ├── pypi-publish-agent.yml
│       ├── pypi-publish-computer-server.yml
│       ├── pypi-publish-computer.yml
│       ├── pypi-publish-core.yml
│       ├── pypi-publish-mcp-server.yml
│       ├── pypi-publish-pylume.yml
│       ├── pypi-publish-som.yml
│       ├── pypi-reusable-publish.yml
│       └── test-validation-script.yml
├── .gitignore
├── .vscode
│   ├── docs.code-workspace
│   ├── launch.json
│   ├── libs-ts.code-workspace
│   ├── lume.code-workspace
│   ├── lumier.code-workspace
│   └── py.code-workspace
├── blog
│   ├── app-use.md
│   ├── assets
│   │   ├── composite-agents.png
│   │   ├── docker-ubuntu-support.png
│   │   ├── hack-booth.png
│   │   ├── hack-closing-ceremony.jpg
│   │   ├── hack-cua-ollama-hud.jpeg
│   │   ├── hack-leaderboard.png
│   │   ├── hack-the-north.png
│   │   ├── hack-winners.jpeg
│   │   ├── hack-workshop.jpeg
│   │   ├── hud-agent-evals.png
│   │   └── trajectory-viewer.jpeg
│   ├── bringing-computer-use-to-the-web.md
│   ├── build-your-own-operator-on-macos-1.md
│   ├── build-your-own-operator-on-macos-2.md
│   ├── composite-agents.md
│   ├── cua-hackathon.md
│   ├── hack-the-north.md
│   ├── hud-agent-evals.md
│   ├── human-in-the-loop.md
│   ├── introducing-cua-cloud-containers.md
│   ├── lume-to-containerization.md
│   ├── sandboxed-python-execution.md
│   ├── training-computer-use-models-trajectories-1.md
│   ├── trajectory-viewer.md
│   ├── ubuntu-docker-support.md
│   └── windows-sandbox.md
├── CONTRIBUTING.md
├── Development.md
├── Dockerfile
├── docs
│   ├── .gitignore
│   ├── .prettierrc
│   ├── content
│   │   └── docs
│   │       ├── agent-sdk
│   │       │   ├── agent-loops.mdx
│   │       │   ├── benchmarks
│   │       │   │   ├── index.mdx
│   │       │   │   ├── interactive.mdx
│   │       │   │   ├── introduction.mdx
│   │       │   │   ├── meta.json
│   │       │   │   ├── osworld-verified.mdx
│   │       │   │   ├── screenspot-pro.mdx
│   │       │   │   └── screenspot-v2.mdx
│   │       │   ├── callbacks
│   │       │   │   ├── agent-lifecycle.mdx
│   │       │   │   ├── cost-saving.mdx
│   │       │   │   ├── index.mdx
│   │       │   │   ├── logging.mdx
│   │       │   │   ├── meta.json
│   │       │   │   ├── pii-anonymization.mdx
│   │       │   │   └── trajectories.mdx
│   │       │   ├── chat-history.mdx
│   │       │   ├── custom-computer-handlers.mdx
│   │       │   ├── custom-tools.mdx
│   │       │   ├── customizing-computeragent.mdx
│   │       │   ├── integrations
│   │       │   │   ├── hud.mdx
│   │       │   │   └── meta.json
│   │       │   ├── message-format.mdx
│   │       │   ├── meta.json
│   │       │   ├── migration-guide.mdx
│   │       │   ├── prompt-caching.mdx
│   │       │   ├── supported-agents
│   │       │   │   ├── composed-agents.mdx
│   │       │   │   ├── computer-use-agents.mdx
│   │       │   │   ├── grounding-models.mdx
│   │       │   │   ├── human-in-the-loop.mdx
│   │       │   │   └── meta.json
│   │       │   ├── supported-model-providers
│   │       │   │   ├── index.mdx
│   │       │   │   └── local-models.mdx
│   │       │   └── usage-tracking.mdx
│   │       ├── computer-sdk
│   │       │   ├── commands.mdx
│   │       │   ├── computer-ui.mdx
│   │       │   ├── computers.mdx
│   │       │   ├── meta.json
│   │       │   └── sandboxed-python.mdx
│   │       ├── index.mdx
│   │       ├── libraries
│   │       │   ├── agent
│   │       │   │   └── index.mdx
│   │       │   ├── computer
│   │       │   │   └── index.mdx
│   │       │   ├── computer-server
│   │       │   │   ├── Commands.mdx
│   │       │   │   ├── index.mdx
│   │       │   │   ├── REST-API.mdx
│   │       │   │   └── WebSocket-API.mdx
│   │       │   ├── core
│   │       │   │   └── index.mdx
│   │       │   ├── lume
│   │       │   │   ├── cli-reference.mdx
│   │       │   │   ├── faq.md
│   │       │   │   ├── http-api.mdx
│   │       │   │   ├── index.mdx
│   │       │   │   ├── installation.mdx
│   │       │   │   ├── meta.json
│   │       │   │   └── prebuilt-images.mdx
│   │       │   ├── lumier
│   │       │   │   ├── building-lumier.mdx
│   │       │   │   ├── docker-compose.mdx
│   │       │   │   ├── docker.mdx
│   │       │   │   ├── index.mdx
│   │       │   │   ├── installation.mdx
│   │       │   │   └── meta.json
│   │       │   ├── mcp-server
│   │       │   │   ├── client-integrations.mdx
│   │       │   │   ├── configuration.mdx
│   │       │   │   ├── index.mdx
│   │       │   │   ├── installation.mdx
│   │       │   │   ├── llm-integrations.mdx
│   │       │   │   ├── meta.json
│   │       │   │   ├── tools.mdx
│   │       │   │   └── usage.mdx
│   │       │   └── som
│   │       │       ├── configuration.mdx
│   │       │       └── index.mdx
│   │       ├── meta.json
│   │       ├── quickstart-cli.mdx
│   │       ├── quickstart-devs.mdx
│   │       └── telemetry.mdx
│   ├── next.config.mjs
│   ├── package-lock.json
│   ├── package.json
│   ├── pnpm-lock.yaml
│   ├── postcss.config.mjs
│   ├── public
│   │   └── img
│   │       ├── agent_gradio_ui.png
│   │       ├── agent.png
│   │       ├── cli.png
│   │       ├── computer.png
│   │       ├── som_box_threshold.png
│   │       └── som_iou_threshold.png
│   ├── README.md
│   ├── source.config.ts
│   ├── src
│   │   ├── app
│   │   │   ├── (home)
│   │   │   │   ├── [[...slug]]
│   │   │   │   │   └── page.tsx
│   │   │   │   └── layout.tsx
│   │   │   ├── api
│   │   │   │   └── search
│   │   │   │       └── route.ts
│   │   │   ├── favicon.ico
│   │   │   ├── global.css
│   │   │   ├── layout.config.tsx
│   │   │   ├── layout.tsx
│   │   │   ├── llms.mdx
│   │   │   │   └── [[...slug]]
│   │   │   │       └── route.ts
│   │   │   └── llms.txt
│   │   │       └── route.ts
│   │   ├── assets
│   │   │   ├── discord-black.svg
│   │   │   ├── discord-white.svg
│   │   │   ├── logo-black.svg
│   │   │   └── logo-white.svg
│   │   ├── components
│   │   │   ├── iou.tsx
│   │   │   └── mermaid.tsx
│   │   ├── lib
│   │   │   ├── llms.ts
│   │   │   └── source.ts
│   │   └── mdx-components.tsx
│   └── tsconfig.json
├── examples
│   ├── agent_examples.py
│   ├── agent_ui_examples.py
│   ├── computer_examples_windows.py
│   ├── computer_examples.py
│   ├── computer_ui_examples.py
│   ├── computer-example-ts
│   │   ├── .env.example
│   │   ├── .gitignore
│   │   ├── .prettierrc
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── pnpm-lock.yaml
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── helpers.ts
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── docker_examples.py
│   ├── evals
│   │   ├── hud_eval_examples.py
│   │   └── wikipedia_most_linked.txt
│   ├── pylume_examples.py
│   ├── sandboxed_functions_examples.py
│   ├── som_examples.py
│   ├── utils.py
│   └── winsandbox_example.py
├── img
│   ├── agent_gradio_ui.png
│   ├── agent.png
│   ├── cli.png
│   ├── computer.png
│   ├── logo_black.png
│   └── logo_white.png
├── libs
│   ├── kasm
│   │   ├── Dockerfile
│   │   ├── LICENSE
│   │   ├── README.md
│   │   └── src
│   │       └── ubuntu
│   │           └── install
│   │               └── firefox
│   │                   ├── custom_startup.sh
│   │                   ├── firefox.desktop
│   │                   └── install_firefox.sh
│   ├── lume
│   │   ├── .cursorignore
│   │   ├── CONTRIBUTING.md
│   │   ├── Development.md
│   │   ├── img
│   │   │   └── cli.png
│   │   ├── Package.resolved
│   │   ├── Package.swift
│   │   ├── README.md
│   │   ├── resources
│   │   │   └── lume.entitlements
│   │   ├── scripts
│   │   │   ├── build
│   │   │   │   ├── build-debug.sh
│   │   │   │   ├── build-release-notarized.sh
│   │   │   │   └── build-release.sh
│   │   │   └── install.sh
│   │   ├── src
│   │   │   ├── Commands
│   │   │   │   ├── Clone.swift
│   │   │   │   ├── Config.swift
│   │   │   │   ├── Create.swift
│   │   │   │   ├── Delete.swift
│   │   │   │   ├── Get.swift
│   │   │   │   ├── Images.swift
│   │   │   │   ├── IPSW.swift
│   │   │   │   ├── List.swift
│   │   │   │   ├── Logs.swift
│   │   │   │   ├── Options
│   │   │   │   │   └── FormatOption.swift
│   │   │   │   ├── Prune.swift
│   │   │   │   ├── Pull.swift
│   │   │   │   ├── Push.swift
│   │   │   │   ├── Run.swift
│   │   │   │   ├── Serve.swift
│   │   │   │   ├── Set.swift
│   │   │   │   └── Stop.swift
│   │   │   ├── ContainerRegistry
│   │   │   │   ├── ImageContainerRegistry.swift
│   │   │   │   ├── ImageList.swift
│   │   │   │   └── ImagesPrinter.swift
│   │   │   ├── Errors
│   │   │   │   └── Errors.swift
│   │   │   ├── FileSystem
│   │   │   │   ├── Home.swift
│   │   │   │   ├── Settings.swift
│   │   │   │   ├── VMConfig.swift
│   │   │   │   ├── VMDirectory.swift
│   │   │   │   └── VMLocation.swift
│   │   │   ├── LumeController.swift
│   │   │   ├── Main.swift
│   │   │   ├── Server
│   │   │   │   ├── Handlers.swift
│   │   │   │   ├── HTTP.swift
│   │   │   │   ├── Requests.swift
│   │   │   │   ├── Responses.swift
│   │   │   │   └── Server.swift
│   │   │   ├── Utils
│   │   │   │   ├── CommandRegistry.swift
│   │   │   │   ├── CommandUtils.swift
│   │   │   │   ├── Logger.swift
│   │   │   │   ├── NetworkUtils.swift
│   │   │   │   ├── Path.swift
│   │   │   │   ├── ProcessRunner.swift
│   │   │   │   ├── ProgressLogger.swift
│   │   │   │   ├── String.swift
│   │   │   │   └── Utils.swift
│   │   │   ├── Virtualization
│   │   │   │   ├── DarwinImageLoader.swift
│   │   │   │   ├── DHCPLeaseParser.swift
│   │   │   │   ├── ImageLoaderFactory.swift
│   │   │   │   └── VMVirtualizationService.swift
│   │   │   ├── VM
│   │   │   │   ├── DarwinVM.swift
│   │   │   │   ├── LinuxVM.swift
│   │   │   │   ├── VM.swift
│   │   │   │   ├── VMDetails.swift
│   │   │   │   ├── VMDetailsPrinter.swift
│   │   │   │   ├── VMDisplayResolution.swift
│   │   │   │   └── VMFactory.swift
│   │   │   └── VNC
│   │   │       ├── PassphraseGenerator.swift
│   │   │       └── VNCService.swift
│   │   └── tests
│   │       ├── Mocks
│   │       │   ├── MockVM.swift
│   │       │   ├── MockVMVirtualizationService.swift
│   │       │   └── MockVNCService.swift
│   │       ├── VM
│   │       │   └── VMDetailsPrinterTests.swift
│   │       ├── VMTests.swift
│   │       ├── VMVirtualizationServiceTests.swift
│   │       └── VNCServiceTests.swift
│   ├── lumier
│   │   ├── .dockerignore
│   │   ├── Dockerfile
│   │   ├── README.md
│   │   └── src
│   │       ├── bin
│   │       │   └── entry.sh
│   │       ├── config
│   │       │   └── constants.sh
│   │       ├── hooks
│   │       │   └── on-logon.sh
│   │       └── lib
│   │           ├── utils.sh
│   │           └── vm.sh
│   ├── python
│   │   ├── agent
│   │   │   ├── agent
│   │   │   │   ├── __init__.py
│   │   │   │   ├── __main__.py
│   │   │   │   ├── adapters
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── huggingfacelocal_adapter.py
│   │   │   │   │   ├── human_adapter.py
│   │   │   │   │   ├── mlxvlm_adapter.py
│   │   │   │   │   └── models
│   │   │   │   │       ├── __init__.py
│   │   │   │   │       ├── generic.py
│   │   │   │   │       ├── internvl.py
│   │   │   │   │       ├── opencua.py
│   │   │   │   │       └── qwen2_5_vl.py
│   │   │   │   ├── agent.py
│   │   │   │   ├── callbacks
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── base.py
│   │   │   │   │   ├── budget_manager.py
│   │   │   │   │   ├── image_retention.py
│   │   │   │   │   ├── logging.py
│   │   │   │   │   ├── operator_validator.py
│   │   │   │   │   ├── pii_anonymization.py
│   │   │   │   │   ├── prompt_instructions.py
│   │   │   │   │   ├── telemetry.py
│   │   │   │   │   └── trajectory_saver.py
│   │   │   │   ├── cli.py
│   │   │   │   ├── computers
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── base.py
│   │   │   │   │   ├── cua.py
│   │   │   │   │   └── custom.py
│   │   │   │   ├── decorators.py
│   │   │   │   ├── human_tool
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── __main__.py
│   │   │   │   │   ├── server.py
│   │   │   │   │   └── ui.py
│   │   │   │   ├── integrations
│   │   │   │   │   └── hud
│   │   │   │   │       ├── __init__.py
│   │   │   │   │       ├── agent.py
│   │   │   │   │       └── proxy.py
│   │   │   │   ├── loops
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── anthropic.py
│   │   │   │   │   ├── base.py
│   │   │   │   │   ├── composed_grounded.py
│   │   │   │   │   ├── glm45v.py
│   │   │   │   │   ├── gta1.py
│   │   │   │   │   ├── holo.py
│   │   │   │   │   ├── internvl.py
│   │   │   │   │   ├── model_types.csv
│   │   │   │   │   ├── moondream3.py
│   │   │   │   │   ├── omniparser.py
│   │   │   │   │   ├── openai.py
│   │   │   │   │   ├── opencua.py
│   │   │   │   │   └── uitars.py
│   │   │   │   ├── proxy
│   │   │   │   │   ├── examples.py
│   │   │   │   │   └── handlers.py
│   │   │   │   ├── responses.py
│   │   │   │   ├── types.py
│   │   │   │   └── ui
│   │   │   │       ├── __init__.py
│   │   │   │       ├── __main__.py
│   │   │   │       └── gradio
│   │   │   │           ├── __init__.py
│   │   │   │           ├── app.py
│   │   │   │           └── ui_components.py
│   │   │   ├── benchmarks
│   │   │   │   ├── .gitignore
│   │   │   │   ├── contrib.md
│   │   │   │   ├── interactive.py
│   │   │   │   ├── models
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── base.py
│   │   │   │   │   └── gta1.py
│   │   │   │   ├── README.md
│   │   │   │   ├── ss-pro.py
│   │   │   │   ├── ss-v2.py
│   │   │   │   └── utils.py
│   │   │   ├── example.py
│   │   │   ├── pyproject.toml
│   │   │   └── README.md
│   │   ├── computer
│   │   │   ├── computer
│   │   │   │   ├── __init__.py
│   │   │   │   ├── computer.py
│   │   │   │   ├── diorama_computer.py
│   │   │   │   ├── helpers.py
│   │   │   │   ├── interface
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── base.py
│   │   │   │   │   ├── factory.py
│   │   │   │   │   ├── generic.py
│   │   │   │   │   ├── linux.py
│   │   │   │   │   ├── macos.py
│   │   │   │   │   ├── models.py
│   │   │   │   │   └── windows.py
│   │   │   │   ├── logger.py
│   │   │   │   ├── models.py
│   │   │   │   ├── providers
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── base.py
│   │   │   │   │   ├── cloud
│   │   │   │   │   │   ├── __init__.py
│   │   │   │   │   │   └── provider.py
│   │   │   │   │   ├── docker
│   │   │   │   │   │   ├── __init__.py
│   │   │   │   │   │   └── provider.py
│   │   │   │   │   ├── factory.py
│   │   │   │   │   ├── lume
│   │   │   │   │   │   ├── __init__.py
│   │   │   │   │   │   └── provider.py
│   │   │   │   │   ├── lume_api.py
│   │   │   │   │   ├── lumier
│   │   │   │   │   │   ├── __init__.py
│   │   │   │   │   │   └── provider.py
│   │   │   │   │   └── winsandbox
│   │   │   │   │       ├── __init__.py
│   │   │   │   │       ├── provider.py
│   │   │   │   │       └── setup_script.ps1
│   │   │   │   ├── ui
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── __main__.py
│   │   │   │   │   └── gradio
│   │   │   │   │       ├── __init__.py
│   │   │   │   │       └── app.py
│   │   │   │   └── utils.py
│   │   │   ├── poetry.toml
│   │   │   ├── pyproject.toml
│   │   │   └── README.md
│   │   ├── computer-server
│   │   │   ├── computer_server
│   │   │   │   ├── __init__.py
│   │   │   │   ├── __main__.py
│   │   │   │   ├── cli.py
│   │   │   │   ├── diorama
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── base.py
│   │   │   │   │   ├── diorama_computer.py
│   │   │   │   │   ├── diorama.py
│   │   │   │   │   ├── draw.py
│   │   │   │   │   ├── macos.py
│   │   │   │   │   └── safezone.py
│   │   │   │   ├── handlers
│   │   │   │   │   ├── base.py
│   │   │   │   │   ├── factory.py
│   │   │   │   │   ├── generic.py
│   │   │   │   │   ├── linux.py
│   │   │   │   │   ├── macos.py
│   │   │   │   │   └── windows.py
│   │   │   │   ├── main.py
│   │   │   │   ├── server.py
│   │   │   │   └── watchdog.py
│   │   │   ├── examples
│   │   │   │   ├── __init__.py
│   │   │   │   └── usage_example.py
│   │   │   ├── pyproject.toml
│   │   │   ├── README.md
│   │   │   ├── run_server.py
│   │   │   └── test_connection.py
│   │   ├── core
│   │   │   ├── core
│   │   │   │   ├── __init__.py
│   │   │   │   └── telemetry
│   │   │   │       ├── __init__.py
│   │   │   │       └── posthog.py
│   │   │   ├── poetry.toml
│   │   │   ├── pyproject.toml
│   │   │   └── README.md
│   │   ├── mcp-server
│   │   │   ├── mcp_server
│   │   │   │   ├── __init__.py
│   │   │   │   ├── __main__.py
│   │   │   │   └── server.py
│   │   │   ├── pyproject.toml
│   │   │   ├── README.md
│   │   │   └── scripts
│   │   │       ├── install_mcp_server.sh
│   │   │       └── start_mcp_server.sh
│   │   ├── pylume
│   │   │   ├── __init__.py
│   │   │   ├── pylume
│   │   │   │   ├── __init__.py
│   │   │   │   ├── client.py
│   │   │   │   ├── exceptions.py
│   │   │   │   ├── lume
│   │   │   │   ├── models.py
│   │   │   │   ├── pylume.py
│   │   │   │   └── server.py
│   │   │   ├── pyproject.toml
│   │   │   └── README.md
│   │   └── som
│   │       ├── LICENSE
│   │       ├── poetry.toml
│   │       ├── pyproject.toml
│   │       ├── README.md
│   │       ├── som
│   │       │   ├── __init__.py
│   │       │   ├── detect.py
│   │       │   ├── detection.py
│   │       │   ├── models.py
│   │       │   ├── ocr.py
│   │       │   ├── util
│   │       │   │   └── utils.py
│   │       │   └── visualization.py
│   │       └── tests
│   │           └── test_omniparser.py
│   ├── typescript
│   │   ├── .gitignore
│   │   ├── .nvmrc
│   │   ├── agent
│   │   │   ├── examples
│   │   │   │   ├── playground-example.html
│   │   │   │   └── README.md
│   │   │   ├── package.json
│   │   │   ├── README.md
│   │   │   ├── src
│   │   │   │   ├── client.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── types.ts
│   │   │   ├── tests
│   │   │   │   └── client.test.ts
│   │   │   ├── tsconfig.json
│   │   │   ├── tsdown.config.ts
│   │   │   └── vitest.config.ts
│   │   ├── biome.json
│   │   ├── computer
│   │   │   ├── .editorconfig
│   │   │   ├── .gitattributes
│   │   │   ├── .gitignore
│   │   │   ├── LICENSE
│   │   │   ├── package.json
│   │   │   ├── README.md
│   │   │   ├── src
│   │   │   │   ├── computer
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── providers
│   │   │   │   │   │   ├── base.ts
│   │   │   │   │   │   ├── cloud.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── interface
│   │   │   │   │   ├── base.ts
│   │   │   │   │   ├── factory.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── linux.ts
│   │   │   │   │   ├── macos.ts
│   │   │   │   │   └── windows.ts
│   │   │   │   └── types.ts
│   │   │   ├── tests
│   │   │   │   ├── computer
│   │   │   │   │   └── cloud.test.ts
│   │   │   │   ├── interface
│   │   │   │   │   ├── factory.test.ts
│   │   │   │   │   ├── index.test.ts
│   │   │   │   │   ├── linux.test.ts
│   │   │   │   │   ├── macos.test.ts
│   │   │   │   │   └── windows.test.ts
│   │   │   │   └── setup.ts
│   │   │   ├── tsconfig.json
│   │   │   ├── tsdown.config.ts
│   │   │   └── vitest.config.ts
│   │   ├── core
│   │   │   ├── .editorconfig
│   │   │   ├── .gitattributes
│   │   │   ├── .gitignore
│   │   │   ├── LICENSE
│   │   │   ├── package.json
│   │   │   ├── README.md
│   │   │   ├── src
│   │   │   │   ├── index.ts
│   │   │   │   └── telemetry
│   │   │   │       ├── clients
│   │   │   │       │   ├── index.ts
│   │   │   │       │   └── posthog.ts
│   │   │   │       └── index.ts
│   │   │   ├── tests
│   │   │   │   └── telemetry.test.ts
│   │   │   ├── tsconfig.json
│   │   │   ├── tsdown.config.ts
│   │   │   └── vitest.config.ts
│   │   ├── package.json
│   │   ├── pnpm-lock.yaml
│   │   ├── pnpm-workspace.yaml
│   │   └── README.md
│   └── xfce
│       ├── .dockerignore
│       ├── .gitignore
│       ├── Dockerfile
│       ├── README.md
│       └── src
│           ├── scripts
│           │   ├── resize-display.sh
│           │   ├── start-computer-server.sh
│           │   ├── start-novnc.sh
│           │   ├── start-vnc.sh
│           │   └── xstartup.sh
│           ├── supervisor
│           │   └── supervisord.conf
│           └── xfce-config
│               ├── helpers.rc
│               ├── xfce4-power-manager.xml
│               └── xfce4-session.xml
├── LICENSE.md
├── notebooks
│   ├── agent_nb.ipynb
│   ├── blog
│   │   ├── build-your-own-operator-on-macos-1.ipynb
│   │   └── build-your-own-operator-on-macos-2.ipynb
│   ├── composite_agents_docker_nb.ipynb
│   ├── computer_nb.ipynb
│   ├── computer_server_nb.ipynb
│   ├── customizing_computeragent.ipynb
│   ├── eval_osworld.ipynb
│   ├── ollama_nb.ipynb
│   ├── pylume_nb.ipynb
│   ├── README.md
│   ├── sota_hackathon_cloud.ipynb
│   └── sota_hackathon.ipynb
├── pdm.lock
├── pyproject.toml
├── pyrightconfig.json
├── README.md
├── samples
│   └── community
│       ├── global-online
│       │   └── README.md
│       └── hack-the-north
│           └── README.md
├── scripts
│   ├── build-uv.sh
│   ├── build.ps1
│   ├── build.sh
│   ├── cleanup.sh
│   ├── playground-docker.sh
│   ├── playground.sh
│   └── run-docker-dev.sh
└── tests
    ├── pytest.ini
    ├── shell_cmd.py
    ├── test_files.py
    ├── test_shell_bash.py
    ├── test_telemetry.py
    ├── test_venv.py
    └── test_watchdog.py
```

# Files

--------------------------------------------------------------------------------
/libs/lume/src/ContainerRegistry/ImageContainerRegistry.swift:
--------------------------------------------------------------------------------

```swift
   1 | import ArgumentParser
   2 | import CommonCrypto
   3 | import Compression  // Add this import
   4 | import Darwin
   5 | import Foundation
   6 | import Swift
   7 | 
   8 | // Extension to calculate SHA256 hash
   9 | extension Data {
  10 |     func sha256String() -> String {
  11 |         let hash = self.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [UInt8] in
  12 |             var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
  13 |             CC_SHA256(bytes.baseAddress, CC_LONG(self.count), &hash)
  14 |             return hash
  15 |         }
  16 |         return hash.map { String(format: "%02x", $0) }.joined()
  17 |     }
  18 | }
  19 | 
  20 | // Push-related errors
  21 | enum PushError: Error {
  22 |     case uploadInitiationFailed
  23 |     case blobUploadFailed
  24 |     case manifestPushFailed
  25 |     case authenticationFailed
  26 |     case missingToken
  27 |     case invalidURL
  28 |     case lz4NotFound  // Added error case
  29 |     case invalidMediaType  // Added during part refactoring
  30 |     case missingUncompressedSizeAnnotation  // Added for sparse file handling
  31 |     case fileCreationFailed(String)  // Added for sparse file handling
  32 |     case reassemblySetupFailed(path: String, underlyingError: Error?)  // Added for sparse file handling
  33 |     case missingPart(Int)  // Added for sparse file handling
  34 |     case layerDownloadFailed(String)  // Added for download retries
  35 |     case manifestFetchFailed  // Added for manifest fetching
  36 |     case insufficientPermissions(String)  // Added for permission issues
  37 | }
  38 | 
  39 | // Define a specific error type for when no underlying error exists
  40 | struct NoSpecificUnderlyingError: Error, CustomStringConvertible {
  41 |     var description: String { "No specific underlying error was provided." }
  42 | }
  43 | 
  44 | struct ChunkMetadata: Codable {
  45 |     let uncompressedDigest: String
  46 |     let uncompressedSize: UInt64
  47 |     let compressedDigest: String
  48 |     let compressedSize: Int
  49 | }
  50 | 
  51 | // Define struct to decode relevant parts of config.json
  52 | struct OCIManifestLayer {
  53 |     let mediaType: String
  54 |     let size: Int
  55 |     let digest: String
  56 |     let uncompressedSize: UInt64?
  57 |     let uncompressedContentDigest: String?
  58 | 
  59 |     init(
  60 |         mediaType: String, size: Int, digest: String, uncompressedSize: UInt64? = nil,
  61 |         uncompressedContentDigest: String? = nil
  62 |     ) {
  63 |         self.mediaType = mediaType
  64 |         self.size = size
  65 |         self.digest = digest
  66 |         self.uncompressedSize = uncompressedSize
  67 |         self.uncompressedContentDigest = uncompressedContentDigest
  68 |     }
  69 | }
  70 | 
  71 | struct OCIConfig: Codable {
  72 |     struct Annotations: Codable {
  73 |         let uncompressedSize: String?  // Use optional String
  74 | 
  75 |         enum CodingKeys: String, CodingKey {
  76 |             case uncompressedSize = "com.trycua.lume.disk.uncompressed_size"
  77 |         }
  78 |     }
  79 |     let annotations: Annotations?  // Optional annotations
  80 | }
  81 | 
  82 | struct Layer: Codable, Equatable {
  83 |     let mediaType: String
  84 |     let digest: String
  85 |     let size: Int
  86 | }
  87 | 
  88 | struct Manifest: Codable {
  89 |     let layers: [Layer]
  90 |     let config: Layer?
  91 |     let mediaType: String
  92 |     let schemaVersion: Int
  93 | }
  94 | 
  95 | struct RepositoryTag: Codable {
  96 |     let name: String
  97 |     let tags: [String]
  98 | }
  99 | 
 100 | struct RepositoryList: Codable {
 101 |     let repositories: [String]
 102 | }
 103 | 
 104 | struct RepositoryTags: Codable {
 105 |     let name: String
 106 |     let tags: [String]
 107 | }
 108 | 
 109 | struct CachedImage {
 110 |     let repository: String
 111 |     let imageId: String
 112 |     let manifestId: String
 113 | }
 114 | 
 115 | struct ImageMetadata: Codable {
 116 |     let image: String
 117 |     let manifestId: String
 118 |     let timestamp: Date
 119 | }
 120 | 
 121 | // Actor to safely collect disk part information from concurrent tasks
 122 | actor DiskPartsCollector {
 123 |     // Store tuples of (sequentialPartNum, url)
 124 |     private var diskParts: [(Int, URL)] = []
 125 |     // Restore internal counter
 126 |     private var partCounter = 0
 127 | 
 128 |     // Adds a part and returns its assigned sequential number
 129 |     func addPart(url: URL) -> Int {
 130 |         partCounter += 1  // Use counter logic
 131 |         let partNum = partCounter
 132 |         diskParts.append((partNum, url))  // Store sequential number
 133 |         return partNum  // Return assigned sequential number
 134 |     }
 135 | 
 136 |     // Sort by the sequential part number (index 0 of tuple)
 137 |     func getSortedParts() -> [(Int, URL)] {
 138 |         return diskParts.sorted { $0.0 < $1.0 }
 139 |     }
 140 | 
 141 |     // Restore getTotalParts
 142 |     func getTotalParts() -> Int {
 143 |         return partCounter
 144 |     }
 145 | }
 146 | 
 147 | actor ProgressTracker {
 148 |     private var totalBytes: Int64 = 0
 149 |     private var downloadedBytes: Int64 = 0
 150 |     private var progressLogger = ProgressLogger(threshold: 0.01)
 151 |     private var totalFiles: Int = 0
 152 |     private var completedFiles: Int = 0
 153 | 
 154 |     // Download speed tracking
 155 |     private var startTime: Date = Date()
 156 |     private var lastUpdateTime: Date = Date()
 157 |     private var lastUpdateBytes: Int64 = 0
 158 |     private var speedSamples: [Double] = []
 159 |     private var peakSpeed: Double = 0
 160 |     private var totalElapsedTime: TimeInterval = 0
 161 | 
 162 |     // Smoothing factor for speed calculation
 163 |     private var speedSmoothing: Double = 0.3
 164 |     private var smoothedSpeed: Double = 0
 165 | 
 166 |     func setTotal(_ total: Int64, files: Int) {
 167 |         totalBytes = total
 168 |         totalFiles = files
 169 |         startTime = Date()
 170 |         lastUpdateTime = startTime
 171 |         smoothedSpeed = 0
 172 |     }
 173 | 
 174 |     func addProgress(_ bytes: Int64) {
 175 |         downloadedBytes += bytes
 176 |         let now = Date()
 177 |         let elapsed = now.timeIntervalSince(lastUpdateTime)
 178 | 
 179 |         // Show first progress update immediately, then throttle updates
 180 |         let shouldUpdate = (downloadedBytes <= bytes) || (elapsed >= 0.5)
 181 | 
 182 |         if shouldUpdate {
 183 |             let currentSpeed = Double(downloadedBytes - lastUpdateBytes) / max(elapsed, 0.001)
 184 |             speedSamples.append(currentSpeed)
 185 | 
 186 |             // Cap samples array to prevent memory growth
 187 |             if speedSamples.count > 20 {
 188 |                 speedSamples.removeFirst(speedSamples.count - 20)
 189 |             }
 190 | 
 191 |             // Update peak speed
 192 |             peakSpeed = max(peakSpeed, currentSpeed)
 193 | 
 194 |             // Apply exponential smoothing to the speed
 195 |             if smoothedSpeed == 0 {
 196 |                 smoothedSpeed = currentSpeed
 197 |             } else {
 198 |                 smoothedSpeed = speedSmoothing * currentSpeed + (1 - speedSmoothing) * smoothedSpeed
 199 |             }
 200 | 
 201 |             // Calculate average speed over the last few samples
 202 |             let recentAvgSpeed = calculateAverageSpeed()
 203 | 
 204 |             // Calculate overall average
 205 |             let totalElapsed = now.timeIntervalSince(startTime)
 206 |             let overallAvgSpeed = totalElapsed > 0 ? Double(downloadedBytes) / totalElapsed : 0
 207 | 
 208 |             let progress = Double(downloadedBytes) / Double(totalBytes)
 209 |             logSpeedProgress(
 210 |                 current: progress,
 211 |                 currentSpeed: currentSpeed,
 212 |                 averageSpeed: recentAvgSpeed,
 213 |                 smoothedSpeed: smoothedSpeed,
 214 |                 overallSpeed: overallAvgSpeed,
 215 |                 peakSpeed: peakSpeed,
 216 |                 context: "Downloading Image"
 217 |             )
 218 | 
 219 |             // Update tracking variables
 220 |             lastUpdateTime = now
 221 |             lastUpdateBytes = downloadedBytes
 222 |             totalElapsedTime = totalElapsed
 223 |         }
 224 |     }
 225 | 
 226 |     private func calculateAverageSpeed() -> Double {
 227 |         guard !speedSamples.isEmpty else { return 0 }
 228 | 
 229 |         // Use weighted average giving more emphasis to recent samples
 230 |         var totalWeight = 0.0
 231 |         var weightedSum = 0.0
 232 | 
 233 |         let samples = speedSamples.suffix(min(8, speedSamples.count))
 234 |         for (index, speed) in samples.enumerated() {
 235 |             let weight = Double(index + 1)
 236 |             weightedSum += speed * weight
 237 |             totalWeight += weight
 238 |         }
 239 | 
 240 |         return totalWeight > 0 ? weightedSum / totalWeight : 0
 241 |     }
 242 | 
 243 |     func getDownloadStats() -> DownloadStats {
 244 |         let avgSpeed = totalElapsedTime > 0 ? Double(downloadedBytes) / totalElapsedTime : 0
 245 |         return DownloadStats(
 246 |             totalBytes: totalBytes,
 247 |             downloadedBytes: downloadedBytes,
 248 |             elapsedTime: totalElapsedTime,
 249 |             averageSpeed: avgSpeed,
 250 |             peakSpeed: peakSpeed
 251 |         )
 252 |     }
 253 | 
 254 |     private func logSpeedProgress(
 255 |         current: Double,
 256 |         currentSpeed: Double,
 257 |         averageSpeed: Double,
 258 |         smoothedSpeed: Double,
 259 |         overallSpeed: Double,
 260 |         peakSpeed: Double,
 261 |         context: String
 262 |     ) {
 263 |         let progressPercent = Int(current * 100)
 264 |         let currentSpeedStr = formatByteSpeed(currentSpeed)
 265 |         let avgSpeedStr = formatByteSpeed(averageSpeed)
 266 |         let peakSpeedStr = formatByteSpeed(peakSpeed)
 267 | 
 268 |         // Calculate ETA based on the smoothed speed which is more stable
 269 |         // This provides a more realistic estimate that doesn't fluctuate as much
 270 |         let remainingBytes = totalBytes - downloadedBytes
 271 |         let speedForEta = max(smoothedSpeed, averageSpeed * 0.8)  // Use the higher of smoothed or 80% of avg
 272 |         let etaSeconds = speedForEta > 0 ? Double(remainingBytes) / speedForEta : 0
 273 |         let etaStr = formatTimeRemaining(etaSeconds)
 274 | 
 275 |         let progressBar = createProgressBar(progress: current)
 276 | 
 277 |         print(
 278 |             "\r\(progressBar) \(progressPercent)% | Current: \(currentSpeedStr) | Avg: \(avgSpeedStr) | Peak: \(peakSpeedStr) | ETA: \(etaStr)     ",
 279 |             terminator: "")
 280 |         fflush(stdout)
 281 |     }
 282 | 
 283 |     private func createProgressBar(progress: Double, width: Int = 30) -> String {
 284 |         let completedWidth = Int(progress * Double(width))
 285 |         let remainingWidth = width - completedWidth
 286 | 
 287 |         let completed = String(repeating: "█", count: completedWidth)
 288 |         let remaining = String(repeating: "░", count: remainingWidth)
 289 | 
 290 |         return "[\(completed)\(remaining)]"
 291 |     }
 292 | 
 293 |     private func formatByteSpeed(_ bytesPerSecond: Double) -> String {
 294 |         let units = ["B/s", "KB/s", "MB/s", "GB/s"]
 295 |         var speed = bytesPerSecond
 296 |         var unitIndex = 0
 297 | 
 298 |         while speed > 1024 && unitIndex < units.count - 1 {
 299 |             speed /= 1024
 300 |             unitIndex += 1
 301 |         }
 302 | 
 303 |         return String(format: "%.1f %@", speed, units[unitIndex])
 304 |     }
 305 | 
 306 |     private func formatTimeRemaining(_ seconds: Double) -> String {
 307 |         if seconds.isNaN || seconds.isInfinite || seconds <= 0 {
 308 |             return "calculating..."
 309 |         }
 310 | 
 311 |         let hours = Int(seconds) / 3600
 312 |         let minutes = (Int(seconds) % 3600) / 60
 313 |         let secs = Int(seconds) % 60
 314 | 
 315 |         if hours > 0 {
 316 |             return String(format: "%d:%02d:%02d", hours, minutes, secs)
 317 |         } else {
 318 |             return String(format: "%d:%02d", minutes, secs)
 319 |         }
 320 |     }
 321 | }
 322 | 
 323 | struct DownloadStats {
 324 |     let totalBytes: Int64
 325 |     let downloadedBytes: Int64
 326 |     let elapsedTime: TimeInterval
 327 |     let averageSpeed: Double
 328 |     let peakSpeed: Double
 329 | 
 330 |     func formattedSummary() -> String {
 331 |         let bytesStr = ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .file)
 332 |         let avgSpeedStr = formatSpeed(averageSpeed)
 333 |         let peakSpeedStr = formatSpeed(peakSpeed)
 334 |         let timeStr = formatTime(elapsedTime)
 335 | 
 336 |         return """
 337 |             Download Statistics:
 338 |             - Total downloaded: \(bytesStr)
 339 |             - Elapsed time: \(timeStr)
 340 |             - Average speed: \(avgSpeedStr)
 341 |             - Peak speed: \(peakSpeedStr)
 342 |             """
 343 |     }
 344 | 
 345 |     private func formatSpeed(_ bytesPerSecond: Double) -> String {
 346 |         let formatter = ByteCountFormatter()
 347 |         formatter.countStyle = .file
 348 |         let bytesStr = formatter.string(fromByteCount: Int64(bytesPerSecond))
 349 |         return "\(bytesStr)/s"
 350 |     }
 351 | 
 352 |     private func formatTime(_ seconds: TimeInterval) -> String {
 353 |         let hours = Int(seconds) / 3600
 354 |         let minutes = (Int(seconds) % 3600) / 60
 355 |         let secs = Int(seconds) % 60
 356 | 
 357 |         if hours > 0 {
 358 |             return String(format: "%d hours, %d minutes, %d seconds", hours, minutes, secs)
 359 |         } else if minutes > 0 {
 360 |             return String(format: "%d minutes, %d seconds", minutes, secs)
 361 |         } else {
 362 |             return String(format: "%d seconds", secs)
 363 |         }
 364 |     }
 365 | }
 366 | 
 367 | // Renamed struct
 368 | struct UploadStats {
 369 |     let totalBytes: Int64
 370 |     let uploadedBytes: Int64  // Renamed
 371 |     let elapsedTime: TimeInterval
 372 |     let averageSpeed: Double
 373 |     let peakSpeed: Double
 374 | 
 375 |     func formattedSummary() -> String {
 376 |         let bytesStr = ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .file)
 377 |         let avgSpeedStr = formatSpeed(averageSpeed)
 378 |         let peakSpeedStr = formatSpeed(peakSpeed)
 379 |         let timeStr = formatTime(elapsedTime)
 380 |         return """
 381 |             Upload Statistics:
 382 |             - Total uploaded: \(bytesStr)
 383 |             - Elapsed time: \(timeStr)
 384 |             - Average speed: \(avgSpeedStr)
 385 |             - Peak speed: \(peakSpeedStr)
 386 |             """
 387 |     }
 388 |     private func formatSpeed(_ bytesPerSecond: Double) -> String {
 389 |         let formatter = ByteCountFormatter()
 390 |         formatter.countStyle = .file
 391 |         let bytesStr = formatter.string(fromByteCount: Int64(bytesPerSecond))
 392 |         return "\(bytesStr)/s"
 393 |     }
 394 |     private func formatTime(_ seconds: TimeInterval) -> String {
 395 |         let hours = Int(seconds) / 3600
 396 |         let minutes = (Int(seconds) % 3600) / 60
 397 |         let secs = Int(seconds) % 60
 398 |         if hours > 0 {
 399 |             return String(format: "%d hours, %d minutes, %d seconds", hours, minutes, secs)
 400 |         } else if minutes > 0 {
 401 |             return String(format: "%d minutes, %d seconds", minutes, secs)
 402 |         } else {
 403 |             return String(format: "%d seconds", secs)
 404 |         }
 405 |     }
 406 | }
 407 | 
 408 | actor TaskCounter {
 409 |     private var count: Int = 0
 410 | 
 411 |     func increment() { count += 1 }
 412 |     func decrement() { count -= 1 }
 413 |     func current() -> Int { count }
 414 | }
 415 | 
 416 | class ImageContainerRegistry: @unchecked Sendable {
 417 |     private let registry: String
 418 |     private let organization: String
 419 |     private let downloadProgress = ProgressTracker()  // Renamed for clarity
 420 |     private let uploadProgress = UploadProgressTracker()  // Added upload tracker
 421 |     private let cacheDirectory: URL
 422 |     private let downloadLock = NSLock()
 423 |     private var activeDownloads: [String] = []
 424 |     private let cachingEnabled: Bool
 425 | 
 426 |     // Constants for zero-skipping write logic
 427 |     private static let holeGranularityBytes = 4 * 1024 * 1024  // 4MB block size for checking zeros
 428 |     private static let zeroChunk = Data(count: holeGranularityBytes)
 429 | 
 430 |     // Add the createProgressBar function here as a private method
 431 |     private func createProgressBar(progress: Double, width: Int = 30) -> String {
 432 |         let completedWidth = Int(progress * Double(width))
 433 |         let remainingWidth = width - completedWidth
 434 | 
 435 |         let completed = String(repeating: "█", count: completedWidth)
 436 |         let remaining = String(repeating: "░", count: remainingWidth)
 437 | 
 438 |         return "[\(completed)\(remaining)]"
 439 |     }
 440 | 
 441 |     init(registry: String, organization: String) {
 442 |         self.registry = registry
 443 |         self.organization = organization
 444 | 
 445 |         // Get cache directory from settings
 446 |         let cacheDir = SettingsManager.shared.getCacheDirectory()
 447 |         let expandedCacheDir = (cacheDir as NSString).expandingTildeInPath
 448 |         self.cacheDirectory = URL(fileURLWithPath: expandedCacheDir)
 449 |             .appendingPathComponent("ghcr")
 450 | 
 451 |         // Get caching enabled setting
 452 |         self.cachingEnabled = SettingsManager.shared.isCachingEnabled()
 453 | 
 454 |         try? FileManager.default.createDirectory(
 455 |             at: cacheDirectory, withIntermediateDirectories: true)
 456 | 
 457 |         // Create organization directory
 458 |         let orgDir = cacheDirectory.appendingPathComponent(organization)
 459 |         try? FileManager.default.createDirectory(at: orgDir, withIntermediateDirectories: true)
 460 |     }
 461 | 
 462 |     private func getManifestIdentifier(_ manifest: Manifest, manifestDigest: String) -> String {
 463 |         // Use the manifest's own digest as the identifier
 464 |         return manifestDigest.replacingOccurrences(of: ":", with: "_")
 465 |     }
 466 | 
 467 |     private func getShortImageId(_ digest: String) -> String {
 468 |         // Take first 12 characters of the digest after removing the "sha256:" prefix
 469 |         let id = digest.replacingOccurrences(of: "sha256:", with: "")
 470 |         return String(id.prefix(12))
 471 |     }
 472 | 
 473 |     private func getImageCacheDirectory(manifestId: String) -> URL {
 474 |         return
 475 |             cacheDirectory
 476 |             .appendingPathComponent(organization)
 477 |             .appendingPathComponent(manifestId)
 478 |     }
 479 | 
 480 |     private func getCachedManifestPath(manifestId: String) -> URL {
 481 |         return getImageCacheDirectory(manifestId: manifestId).appendingPathComponent(
 482 |             "manifest.json")
 483 |     }
 484 | 
 485 |     private func getCachedLayerPath(manifestId: String, digest: String) -> URL {
 486 |         return getImageCacheDirectory(manifestId: manifestId).appendingPathComponent(
 487 |             digest.replacingOccurrences(of: ":", with: "_"))
 488 |     }
 489 | 
 490 |     private func setupImageCache(manifestId: String) throws {
 491 |         let cacheDir = getImageCacheDirectory(manifestId: manifestId)
 492 |         // Remove existing cache if it exists
 493 |         if FileManager.default.fileExists(atPath: cacheDir.path) {
 494 |             try FileManager.default.removeItem(at: cacheDir)
 495 |             // Ensure it's completely removed
 496 |             while FileManager.default.fileExists(atPath: cacheDir.path) {
 497 |                 try? FileManager.default.removeItem(at: cacheDir)
 498 |             }
 499 |         }
 500 |         try FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
 501 |     }
 502 | 
 503 |     private func loadCachedManifest(manifestId: String) -> Manifest? {
 504 |         let manifestPath = getCachedManifestPath(manifestId: manifestId)
 505 |         guard let data = try? Data(contentsOf: manifestPath) else { return nil }
 506 |         return try? JSONDecoder().decode(Manifest.self, from: data)
 507 |     }
 508 | 
 509 |     private func validateCache(manifest: Manifest, manifestId: String) -> Bool {
 510 |         // Skip cache validation if caching is disabled
 511 |         if !cachingEnabled {
 512 |             return false
 513 |         }
 514 | 
 515 |         // Check if we have a reassembled image
 516 |         let reassembledCachePath = getImageCacheDirectory(manifestId: manifestId)
 517 |             .appendingPathComponent("disk.img.reassembled")
 518 |         if FileManager.default.fileExists(atPath: reassembledCachePath.path) {
 519 |             Logger.info("Found reassembled disk image in cache validation")
 520 | 
 521 |             // If we have a reassembled image, we only need to make sure the manifest matches
 522 |             guard let cachedManifest = loadCachedManifest(manifestId: manifestId),
 523 |                 cachedManifest.layers == manifest.layers
 524 |             else {
 525 |                 return false
 526 |             }
 527 | 
 528 |             // We have a reassembled image and the manifest matches
 529 |             return true
 530 |         }
 531 | 
 532 |         // If no reassembled image, check layer files
 533 |         // First check if manifest exists and matches
 534 |         guard let cachedManifest = loadCachedManifest(manifestId: manifestId),
 535 |             cachedManifest.layers == manifest.layers
 536 |         else {
 537 |             return false
 538 |         }
 539 | 
 540 |         // Then verify all layer files exist
 541 |         for layer in manifest.layers {
 542 |             let cachedLayer = getCachedLayerPath(manifestId: manifestId, digest: layer.digest)
 543 |             if !FileManager.default.fileExists(atPath: cachedLayer.path) {
 544 |                 return false
 545 |             }
 546 |         }
 547 | 
 548 |         return true
 549 |     }
 550 | 
 551 |     private func saveManifest(_ manifest: Manifest, manifestId: String) throws {
 552 |         // Skip saving manifest if caching is disabled
 553 |         if !cachingEnabled {
 554 |             return
 555 |         }
 556 | 
 557 |         let manifestPath = getCachedManifestPath(manifestId: manifestId)
 558 |         try JSONEncoder().encode(manifest).write(to: manifestPath)
 559 |     }
 560 | 
 561 |     private func isDownloading(_ digest: String) -> Bool {
 562 |         downloadLock.lock()
 563 |         defer { downloadLock.unlock() }
 564 |         return activeDownloads.contains(digest)
 565 |     }
 566 | 
 567 |     private func markDownloadStarted(_ digest: String) {
 568 |         downloadLock.lock()
 569 |         if !activeDownloads.contains(digest) {
 570 |             activeDownloads.append(digest)
 571 |         }
 572 |         downloadLock.unlock()
 573 |     }
 574 | 
 575 |     private func markDownloadComplete(_ digest: String) {
 576 |         downloadLock.lock()
 577 |         activeDownloads.removeAll { $0 == digest }
 578 |         downloadLock.unlock()
 579 |     }
 580 | 
 581 |     private func waitForExistingDownload(_ digest: String, cachedLayer: URL) async throws {
 582 |         while isDownloading(digest) {
 583 |             try await Task.sleep(nanoseconds: 1_000_000_000)  // Sleep for 1 second
 584 |             if FileManager.default.fileExists(atPath: cachedLayer.path) {
 585 |                 return  // File is now available
 586 |             }
 587 |         }
 588 |     }
 589 | 
 590 |     private func saveImageMetadata(image: String, manifestId: String) throws {
 591 |         // Skip saving metadata if caching is disabled
 592 |         if !cachingEnabled {
 593 |             return
 594 |         }
 595 | 
 596 |         let metadataPath = getImageCacheDirectory(manifestId: manifestId).appendingPathComponent(
 597 |             "metadata.json")
 598 |         let metadata = ImageMetadata(
 599 |             image: image,
 600 |             manifestId: manifestId,
 601 |             timestamp: Date()
 602 |         )
 603 |         try JSONEncoder().encode(metadata).write(to: metadataPath)
 604 |     }
 605 | 
 606 |     private func cleanupOldVersions(currentManifestId: String, image: String) throws {
 607 |         // Skip cleanup if caching is disabled
 608 |         if !cachingEnabled {
 609 |             return
 610 |         }
 611 | 
 612 |         Logger.info(
 613 |             "Checking for old versions of image to clean up",
 614 |             metadata: [
 615 |                 "image": image,
 616 |                 "current_manifest_id": currentManifestId,
 617 |             ])
 618 | 
 619 |         let orgDir = cacheDirectory.appendingPathComponent(organization)
 620 |         guard FileManager.default.fileExists(atPath: orgDir.path) else { return }
 621 | 
 622 |         let contents = try FileManager.default.contentsOfDirectory(atPath: orgDir.path)
 623 |         for item in contents {
 624 |             if item == currentManifestId { continue }
 625 | 
 626 |             let itemPath = orgDir.appendingPathComponent(item)
 627 |             let metadataPath = itemPath.appendingPathComponent("metadata.json")
 628 | 
 629 |             if let metadataData = try? Data(contentsOf: metadataPath),
 630 |                 let metadata = try? JSONDecoder().decode(ImageMetadata.self, from: metadataData)
 631 |             {
 632 |                 if metadata.image == image {
 633 |                     // Before removing, check if there's a reassembled image we should preserve
 634 |                     let reassembledPath = itemPath.appendingPathComponent("disk.img.reassembled")
 635 |                     let nvramPath = itemPath.appendingPathComponent("nvram.bin")
 636 |                     let configPath = itemPath.appendingPathComponent("config.json")
 637 | 
 638 |                     // Preserve reassembled image if it exists
 639 |                     if FileManager.default.fileExists(atPath: reassembledPath.path) {
 640 |                         Logger.info(
 641 |                             "Preserving reassembled disk image during cleanup",
 642 |                             metadata: ["manifest_id": item])
 643 | 
 644 |                         // Ensure the current cache directory exists
 645 |                         let currentCacheDir = getImageCacheDirectory(manifestId: currentManifestId)
 646 |                         try FileManager.default.createDirectory(
 647 |                             at: currentCacheDir, withIntermediateDirectories: true)
 648 | 
 649 |                         // Move reassembled image to current cache directory
 650 |                         let currentReassembledPath = currentCacheDir.appendingPathComponent(
 651 |                             "disk.img.reassembled")
 652 |                         if !FileManager.default.fileExists(atPath: currentReassembledPath.path) {
 653 |                             try FileManager.default.copyItem(
 654 |                                 at: reassembledPath, to: currentReassembledPath)
 655 |                         }
 656 | 
 657 |                         // Also preserve nvram if it exists
 658 |                         if FileManager.default.fileExists(atPath: nvramPath.path) {
 659 |                             let currentNvramPath = currentCacheDir.appendingPathComponent(
 660 |                                 "nvram.bin")
 661 |                             if !FileManager.default.fileExists(atPath: currentNvramPath.path) {
 662 |                                 try FileManager.default.copyItem(
 663 |                                     at: nvramPath, to: currentNvramPath)
 664 |                             }
 665 |                         }
 666 | 
 667 |                         // Also preserve config if it exists
 668 |                         if FileManager.default.fileExists(atPath: configPath.path) {
 669 |                             let currentConfigPath = currentCacheDir.appendingPathComponent(
 670 |                                 "config.json")
 671 |                             if !FileManager.default.fileExists(atPath: currentConfigPath.path) {
 672 |                                 try FileManager.default.copyItem(
 673 |                                     at: configPath, to: currentConfigPath)
 674 |                             }
 675 |                         }
 676 |                     }
 677 | 
 678 |                     // Now remove the old directory
 679 |                     try FileManager.default.removeItem(at: itemPath)
 680 |                     Logger.info(
 681 |                         "Removed old version of image",
 682 |                         metadata: [
 683 |                             "image": image,
 684 |                             "old_manifest_id": item,
 685 |                         ])
 686 |                 }
 687 |                 continue
 688 |             }
 689 | 
 690 |             Logger.info(
 691 |                 "Skipping cleanup check for item without metadata", metadata: ["item": item])
 692 |         }
 693 |     }
 694 | 
 695 |     private func optimizeNetworkSettings() {
 696 |         // Set global URLSession configuration properties for better performance
 697 |         URLSessionConfiguration.default.httpMaximumConnectionsPerHost = 10
 698 |         URLSessionConfiguration.default.httpShouldUsePipelining = true
 699 |         URLSessionConfiguration.default.timeoutIntervalForResource = 3600
 700 | 
 701 |         // Pre-warm DNS resolution
 702 |         let preWarmTask = URLSession.shared.dataTask(with: URL(string: "https://\(self.registry)")!)
 703 |         preWarmTask.resume()
 704 |     }
 705 | 
 706 |     public func pull(
 707 |         image: String,
 708 |         name: String?,
 709 |         locationName: String? = nil
 710 |     ) async throws -> VMDirectory {
 711 |         guard !image.isEmpty else {
 712 |             throw ValidationError("Image name cannot be empty")
 713 |         }
 714 | 
 715 |         let home = Home()
 716 | 
 717 |         // Use provided name or derive from image
 718 |         let vmName = name ?? image.split(separator: ":").first.map(String.init) ?? ""
 719 | 
 720 |         // Determine if locationName is a direct path or a named storage location
 721 |         let vmDir: VMDirectory
 722 |         if let locationName = locationName,
 723 |             locationName.contains("/") || locationName.contains("\\")
 724 |         {
 725 |             // Direct path
 726 |             vmDir = try home.getVMDirectoryFromPath(vmName, storagePath: locationName)
 727 |         } else {
 728 |             // Named storage or default location
 729 |             vmDir = try home.getVMDirectory(vmName, storage: locationName)
 730 |         }
 731 | 
 732 |         // Optimize network early in the process
 733 |         optimizeNetworkSettings()
 734 | 
 735 |         // Parse image name and tag
 736 |         let components = image.split(separator: ":")
 737 |         guard components.count == 2, let tag = components.last else {
 738 |             throw ValidationError("Invalid image format. Expected format: name:tag")
 739 |         }
 740 | 
 741 |         let imageName = String(components.first!)
 742 |         let imageTag = String(tag)
 743 | 
 744 |         Logger.info(
 745 |             "Pulling image",
 746 |             metadata: [
 747 |                 "image": image,
 748 |                 "name": vmName,
 749 |                 "location": locationName ?? "default",
 750 |                 "registry": registry,
 751 |                 "organization": organization,
 752 |             ])
 753 | 
 754 |         // Get anonymous token
 755 |         Logger.info("Getting registry authentication token")
 756 |         let token = try await getToken(
 757 |             repository: "\(self.organization)/\(imageName)", scopes: ["pull"])
 758 | 
 759 |         // Fetch manifest
 760 |         Logger.info("Fetching Image manifest")
 761 |         let (manifest, manifestDigest): (Manifest, String) = try await fetchManifest(
 762 |             repository: "\(self.organization)/\(imageName)",
 763 |             tag: imageTag,
 764 |             token: token
 765 |         )
 766 | 
 767 |         // Get manifest identifier using the manifest's own digest
 768 |         let manifestId = getManifestIdentifier(manifest, manifestDigest: manifestDigest)
 769 | 
 770 |         Logger.info(
 771 |             "Pulling image",
 772 |             metadata: [
 773 |                 "repository": imageName,
 774 |                 "manifest_id": manifestId,
 775 |             ])
 776 | 
 777 |         // Create temporary directory for the entire VM setup
 778 |         let tempVMDir = FileManager.default.temporaryDirectory.appendingPathComponent(
 779 |             "lume_vm_\(UUID().uuidString)")
 780 |         try FileManager.default.createDirectory(at: tempVMDir, withIntermediateDirectories: true)
 781 |         defer {
 782 |             try? FileManager.default.removeItem(at: tempVMDir)
 783 |         }
 784 | 
 785 |         // Check if caching is enabled and if we have a valid cached version
 786 |         Logger.info("Caching enabled: \(cachingEnabled)")
 787 |         if cachingEnabled && validateCache(manifest: manifest, manifestId: manifestId) {
 788 |             Logger.info("Using cached version of image")
 789 |             try await copyFromCache(manifest: manifest, manifestId: manifestId, to: tempVMDir)
 790 |         } else {
 791 |             // If caching is disabled, log it
 792 |             if !cachingEnabled {
 793 |                 Logger.info("Caching is disabled, downloading fresh copy")
 794 |             } else {
 795 |                 Logger.info("Cache miss or invalid cache, setting up new cache")
 796 |             }
 797 | 
 798 |             // Clean up old versions of this repository before setting up new cache if caching is enabled
 799 |             if cachingEnabled {
 800 |                 try cleanupOldVersions(currentManifestId: manifestId, image: imageName)
 801 | 
 802 |                 // Setup new cache directory
 803 |                 try setupImageCache(manifestId: manifestId)
 804 |                 // Save new manifest
 805 |                 try saveManifest(manifest, manifestId: manifestId)
 806 | 
 807 |                 // Save image metadata
 808 |                 try saveImageMetadata(
 809 |                     image: imageName,
 810 |                     manifestId: manifestId
 811 |                 )
 812 |             }
 813 | 
 814 |             // Create temporary directory for new downloads
 815 |             let tempDownloadDir = FileManager.default.temporaryDirectory.appendingPathComponent(
 816 |                 UUID().uuidString)
 817 |             try FileManager.default.createDirectory(
 818 |                 at: tempDownloadDir, withIntermediateDirectories: true)
 819 |             defer {
 820 |                 try? FileManager.default.removeItem(at: tempDownloadDir)
 821 |             }
 822 | 
 823 |             // Set total size and file count
 824 |             let totalFiles = manifest.layers.filter {
 825 |                 $0.mediaType != "application/vnd.oci.empty.v1+json"
 826 |             }.count
 827 |             let totalSize = manifest.layers.reduce(0) { $0 + Int64($1.size) }
 828 |             await downloadProgress.setTotal(totalSize, files: totalFiles)
 829 | 
 830 |             // Process layers with limited concurrency
 831 |             Logger.info("Processing Image layers")
 832 |             Logger.info(
 833 |                 "This may take several minutes depending on the image size and your internet connection. Please wait..."
 834 |             )
 835 | 
 836 |             // Add immediate progress indicator before starting downloads
 837 |             print(
 838 |                 "[░░░░░░░░░░░░░░░░░░░░] 0% | Initializing downloads... | ETA: calculating...     ")
 839 |             fflush(stdout)
 840 | 
 841 |             // Instantiate the collector
 842 |             let diskPartsCollector = DiskPartsCollector()
 843 | 
 844 |             // Adaptive concurrency based on system capabilities
 845 |             let memoryConstrained = determineIfMemoryConstrained()
 846 |             let networkQuality = determineNetworkQuality()
 847 |             let maxConcurrentTasks = calculateOptimalConcurrency(
 848 |                 memoryConstrained: memoryConstrained, networkQuality: networkQuality)
 849 | 
 850 |             Logger.info(
 851 |                 "Using adaptive download configuration: Concurrency=\(maxConcurrentTasks), Memory-optimized=\(memoryConstrained)"
 852 |             )
 853 | 
 854 |             let counter = TaskCounter()
 855 |             var lz4LayerCount = 0  // Count lz4 layers found
 856 | 
 857 |             try await withThrowingTaskGroup(of: Int64.self) { group in
 858 |                 for layer in manifest.layers {
 859 |                     if layer.mediaType == "application/vnd.oci.empty.v1+json" {
 860 |                         continue
 861 |                     }
 862 | 
 863 |                     while await counter.current() >= maxConcurrentTasks {
 864 |                         _ = try await group.next()
 865 |                         await counter.decrement()
 866 |                     }
 867 | 
 868 |                     // Identify disk parts by media type
 869 |                     if layer.mediaType == "application/octet-stream+lz4" {
 870 |                         // --- Handle LZ4 Disk Part Layer ---
 871 |                         lz4LayerCount += 1  // Increment count
 872 |                         let currentPartNum = lz4LayerCount  // Use the current count as the logical number for logging
 873 | 
 874 |                         let cachedLayer = getCachedLayerPath(
 875 |                             manifestId: manifestId, digest: layer.digest)
 876 |                         let digest = layer.digest
 877 |                         let size = layer.size
 878 | 
 879 |                         if memoryConstrained
 880 |                             && FileManager.default.fileExists(atPath: cachedLayer.path)
 881 |                         {
 882 |                             // Add to collector, get sequential number assigned by collector
 883 |                             let collectorPartNum = await diskPartsCollector.addPart(
 884 |                                 url: cachedLayer)
 885 |                             // Log using the sequential number from collector for clarity if needed, or the lz4LayerCount
 886 |                             Logger.info(
 887 |                                 "Using cached lz4 layer (part \(currentPartNum)) directly: \(cachedLayer.lastPathComponent) -> Collector #\(collectorPartNum)"
 888 |                             )
 889 |                             await downloadProgress.addProgress(Int64(size))
 890 |                             continue
 891 |                         } else {
 892 |                             // Download/Copy Path (Task Group)
 893 |                             group.addTask { [self] in
 894 |                                 await counter.increment()
 895 |                                 let finalPath: URL
 896 |                                 if FileManager.default.fileExists(atPath: cachedLayer.path) {
 897 |                                     let tempPartURL = tempDownloadDir.appendingPathComponent(
 898 |                                         "disk.img.part.\(UUID().uuidString)")
 899 |                                     try FileManager.default.copyItem(
 900 |                                         at: cachedLayer, to: tempPartURL)
 901 |                                     await downloadProgress.addProgress(Int64(size))
 902 |                                     finalPath = tempPartURL
 903 |                                 } else {
 904 |                                     let tempPartURL = tempDownloadDir.appendingPathComponent(
 905 |                                         "disk.img.part.\(UUID().uuidString)")
 906 |                                     if isDownloading(digest) {
 907 |                                         try await waitForExistingDownload(
 908 |                                             digest, cachedLayer: cachedLayer)
 909 |                                         if FileManager.default.fileExists(atPath: cachedLayer.path)
 910 |                                         {
 911 |                                             try FileManager.default.copyItem(
 912 |                                                 at: cachedLayer, to: tempPartURL)
 913 |                                             await downloadProgress.addProgress(Int64(size))
 914 |                                             finalPath = tempPartURL
 915 |                                         } else {
 916 |                                             markDownloadStarted(digest)
 917 |                                             try await self.downloadLayer(
 918 |                                                 repository: "\(self.organization)/\(imageName)",
 919 |                                                 digest: digest, mediaType: layer.mediaType,
 920 |                                                 token: token,
 921 |                                                 to: tempPartURL, maxRetries: 5,
 922 |                                                 progress: downloadProgress, manifestId: manifestId
 923 |                                             )
 924 |                                             finalPath = tempPartURL
 925 |                                         }
 926 |                                     } else {
 927 |                                         markDownloadStarted(digest)
 928 |                                         try await self.downloadLayer(
 929 |                                             repository: "\(self.organization)/\(imageName)",
 930 |                                             digest: digest, mediaType: layer.mediaType,
 931 |                                             token: token,
 932 |                                             to: tempPartURL, maxRetries: 5,
 933 |                                             progress: downloadProgress, manifestId: manifestId
 934 |                                         )
 935 |                                         finalPath = tempPartURL
 936 |                                     }
 937 |                                 }
 938 |                                 // Add to collector, get sequential number assigned by collector
 939 |                                 let collectorPartNum = await diskPartsCollector.addPart(
 940 |                                     url: finalPath)
 941 |                                 // Log using the sequential number from collector
 942 |                                 Logger.info(
 943 |                                     "Assigned path for lz4 layer (part \(currentPartNum)): \(finalPath.lastPathComponent) -> Collector #\(collectorPartNum)"
 944 |                                 )
 945 |                                 await counter.decrement()
 946 |                                 return Int64(size)
 947 |                             }
 948 |                         }
 949 |                     } else {
 950 |                         // --- Handle Non-Disk-Part Layer ---
 951 |                         let mediaType = layer.mediaType
 952 |                         let digest = layer.digest
 953 |                         let size = layer.size
 954 | 
 955 |                         // Determine output path based on media type
 956 |                         let outputURL: URL
 957 |                         switch mediaType {
 958 |                         case "application/vnd.oci.image.layer.v1.tar",
 959 |                             "application/octet-stream+gzip":  // Might be compressed disk.img single file?
 960 |                             outputURL = tempDownloadDir.appendingPathComponent("disk.img")
 961 |                         case "application/vnd.oci.image.config.v1+json":
 962 |                             outputURL = tempDownloadDir.appendingPathComponent("config.json")
 963 |                         case "application/octet-stream":  // Could be nvram or uncompressed single disk.img
 964 |                             // Heuristic: If a config.json already exists or is expected, assume this is nvram.
 965 |                             // This might need refinement if single disk images use octet-stream.
 966 |                             if manifest.config != nil {
 967 |                                 outputURL = tempDownloadDir.appendingPathComponent("nvram.bin")
 968 |                             } else {
 969 |                                 // Assume it's a single-file disk image if no config layer is present
 970 |                                 outputURL = tempDownloadDir.appendingPathComponent("disk.img")
 971 |                             }
 972 |                         default:
 973 |                             Logger.info("Skipping unsupported layer media type: \(mediaType)")
 974 |                             continue  // Skip to the next layer
 975 |                         }
 976 | 
 977 |                         // Add task to download/copy the non-disk-part layer
 978 |                         group.addTask { [self] in
 979 |                             await counter.increment()
 980 |                             let cachedLayer = getCachedLayerPath(
 981 |                                 manifestId: manifestId, digest: digest)
 982 | 
 983 |                             if FileManager.default.fileExists(atPath: cachedLayer.path) {
 984 |                                 try FileManager.default.copyItem(at: cachedLayer, to: outputURL)
 985 |                                 await downloadProgress.addProgress(Int64(size))
 986 |                             } else {
 987 |                                 if isDownloading(digest) {
 988 |                                     try await waitForExistingDownload(
 989 |                                         digest, cachedLayer: cachedLayer)
 990 |                                     if FileManager.default.fileExists(atPath: cachedLayer.path) {
 991 |                                         try FileManager.default.copyItem(
 992 |                                             at: cachedLayer, to: outputURL)
 993 |                                         await downloadProgress.addProgress(Int64(size))
 994 |                                         await counter.decrement()  // Decrement before returning
 995 |                                         return Int64(size)
 996 |                                     }
 997 |                                 }
 998 | 
 999 |                                 markDownloadStarted(digest)
1000 |                                 try await self.downloadLayer(
1001 |                                     repository: "\(self.organization)/\(imageName)",
1002 |                                     digest: digest, mediaType: mediaType, token: token,
1003 |                                     to: outputURL, maxRetries: 5,
1004 |                                     progress: downloadProgress, manifestId: manifestId
1005 |                                 )
1006 |                                 // Note: downloadLayer handles caching and marking download complete
1007 |                             }
1008 |                             await counter.decrement()
1009 |                             return Int64(size)
1010 |                         }
1011 |                     }
1012 |                 }  // End for layer in manifest.layers
1013 | 
1014 |                 // Wait for remaining tasks
1015 |                 for try await _ in group {}
1016 |             }  // End TaskGroup
1017 | 
1018 |             // Display download statistics
1019 |             let stats = await downloadProgress.getDownloadStats()
1020 |             Logger.info("")  // New line after progress
1021 |             Logger.info(stats.formattedSummary())
1022 | 
1023 |             // Now that we've downloaded everything to the cache, use copyFromCache to create final VM files
1024 |             if cachingEnabled {
1025 |                 Logger.info("Using copyFromCache method to properly preserve partition tables")
1026 |                 try await copyFromCache(manifest: manifest, manifestId: manifestId, to: tempVMDir)
1027 |             } else {
1028 |                 // Even if caching is disabled, we need to use copyFromCache to assemble the disk image
1029 |                 // correctly with partition tables, then we'll clean up the cache afterward
1030 |                 Logger.info("Caching disabled - using temporary cache to assemble VM files")
1031 |                 try await copyFromCache(manifest: manifest, manifestId: manifestId, to: tempVMDir)
1032 |             }
1033 |         }
1034 | 
1035 |         // Only move to final location once everything is complete
1036 |         if FileManager.default.fileExists(atPath: vmDir.dir.path) {
1037 |             try FileManager.default.removeItem(at: URL(fileURLWithPath: vmDir.dir.path))
1038 |         }
1039 | 
1040 |         // Ensure parent directory exists
1041 |         try FileManager.default.createDirectory(
1042 |             at: URL(fileURLWithPath: vmDir.dir.path).deletingLastPathComponent(),
1043 |             withIntermediateDirectories: true)
1044 | 
1045 |         // Log the final destination
1046 |         Logger.info(
1047 |             "Moving files to VM directory",
1048 |             metadata: [
1049 |                 "destination": vmDir.dir.path,
1050 |                 "location": locationName ?? "default",
1051 |             ])
1052 | 
1053 |         // Move files to final location
1054 |         try FileManager.default.moveItem(at: tempVMDir, to: URL(fileURLWithPath: vmDir.dir.path))
1055 | 
1056 |         // If caching is disabled, clean up the cache entry
1057 |         if !cachingEnabled {
1058 |             Logger.info("Caching disabled - cleaning up temporary cache entry")
1059 |             try? cleanupCacheEntry(manifestId: manifestId)
1060 |         }
1061 | 
1062 |         Logger.info("Download complete: Files extracted to \(vmDir.dir.path)")
1063 |         Logger.info(
1064 |             "Note: Actual disk usage is significantly lower than reported size due to macOS sparse file system"
1065 |         )
1066 |         Logger.info(
1067 |             "Run 'lume run \(vmName)' to reduce the disk image file size by using macOS sparse file system"
1068 |         )
1069 |         return vmDir
1070 |     }
1071 | 
1072 |     // Helper function to clean up a specific cache entry
1073 |     private func cleanupCacheEntry(manifestId: String) throws {
1074 |         let cacheDir = getImageCacheDirectory(manifestId: manifestId)
1075 | 
1076 |         if FileManager.default.fileExists(atPath: cacheDir.path) {
1077 |             Logger.info("Removing cache entry for manifest ID: \(manifestId)")
1078 |             try FileManager.default.removeItem(at: cacheDir)
1079 |         }
1080 |     }
1081 | 
1082 |     // Shared function to handle disk image creation - can be used by both cache hit and cache miss paths
1083 |     private func createDiskImageFromSource(
1084 |         sourceURL: URL,  // Source data to decompress
1085 |         destinationURL: URL,  // Where to create the disk image
1086 |         diskSize: UInt64  // Total size for the sparse file
1087 |     ) throws {
1088 |         Logger.info("Creating sparse disk image...")
1089 | 
1090 |         // Create empty destination file
1091 |         if FileManager.default.fileExists(atPath: destinationURL.path) {
1092 |             try FileManager.default.removeItem(at: destinationURL)
1093 |         }
1094 |         guard FileManager.default.createFile(atPath: destinationURL.path, contents: nil) else {
1095 |             throw PullError.fileCreationFailed(destinationURL.path)
1096 |         }
1097 | 
1098 |         // Create sparse file
1099 |         let outputHandle = try FileHandle(forWritingTo: destinationURL)
1100 |         try outputHandle.truncate(atOffset: diskSize)
1101 | 
1102 |         // Write test patterns at beginning and end
1103 |         Logger.info("Writing test patterns to verify writability...")
1104 |         let testPattern = "LUME_TEST_PATTERN".data(using: .utf8)!
1105 |         try outputHandle.seek(toOffset: 0)
1106 |         try outputHandle.write(contentsOf: testPattern)
1107 |         try outputHandle.seek(toOffset: diskSize - UInt64(testPattern.count))
1108 |         try outputHandle.write(contentsOf: testPattern)
1109 |         try outputHandle.synchronize()
1110 | 
1111 |         // Decompress the source data at offset 0
1112 |         Logger.info("Decompressing source data...")
1113 |         let bytesWritten = try decompressChunkAndWriteSparse(
1114 |             inputPath: sourceURL.path,
1115 |             outputHandle: outputHandle,
1116 |             startOffset: 0
1117 |         )
1118 |         Logger.info(
1119 |             "Decompressed \(ByteCountFormatter.string(fromByteCount: Int64(bytesWritten), countStyle: .file)) of data"
1120 |         )
1121 | 
1122 |         // Ensure data is written and close handle
1123 |         try outputHandle.synchronize()
1124 |         try outputHandle.close()
1125 | 
1126 |         // Run sync to flush filesystem
1127 |         let syncProcess = Process()
1128 |         syncProcess.executableURL = URL(fileURLWithPath: "/bin/sync")
1129 |         try syncProcess.run()
1130 |         syncProcess.waitUntilExit()
1131 | 
1132 |         // Optimize with cp -c
1133 |         if FileManager.default.fileExists(atPath: "/bin/cp") {
1134 |             Logger.info("Optimizing sparse file representation...")
1135 |             let optimizedPath = destinationURL.path + ".optimized"
1136 | 
1137 |             let process = Process()
1138 |             process.executableURL = URL(fileURLWithPath: "/bin/cp")
1139 |             process.arguments = ["-c", destinationURL.path, optimizedPath]
1140 | 
1141 |             try process.run()
1142 |             process.waitUntilExit()
1143 | 
1144 |             if process.terminationStatus == 0 {
1145 |                 // Get optimization results
1146 |                 let optimizedSize =
1147 |                     (try? FileManager.default.attributesOfItem(atPath: optimizedPath)[.size]
1148 |                         as? UInt64) ?? 0
1149 |                 let originalUsage = getActualDiskUsage(path: destinationURL.path)
1150 |                 let optimizedUsage = getActualDiskUsage(path: optimizedPath)
1151 | 
1152 |                 Logger.info(
1153 |                     "Sparse optimization results: Before: \(ByteCountFormatter.string(fromByteCount: Int64(originalUsage), countStyle: .file)) actual usage, After: \(ByteCountFormatter.string(fromByteCount: Int64(optimizedUsage), countStyle: .file)) actual usage (Apparent size: \(ByteCountFormatter.string(fromByteCount: Int64(optimizedSize), countStyle: .file)))"
1154 |                 )
1155 | 
1156 |                 // Replace original with optimized
1157 |                 try FileManager.default.removeItem(at: destinationURL)
1158 |                 try FileManager.default.moveItem(
1159 |                     at: URL(fileURLWithPath: optimizedPath), to: destinationURL)
1160 |                 Logger.info("Replaced with optimized sparse version")
1161 |             } else {
1162 |                 Logger.info("Sparse optimization failed, using original file")
1163 |                 try? FileManager.default.removeItem(atPath: optimizedPath)
1164 |             }
1165 |         }
1166 | 
1167 |         // Set permissions to 0644
1168 |         let chmodProcess = Process()
1169 |         chmodProcess.executableURL = URL(fileURLWithPath: "/bin/chmod")
1170 |         chmodProcess.arguments = ["0644", destinationURL.path]
1171 |         try chmodProcess.run()
1172 |         chmodProcess.waitUntilExit()
1173 | 
1174 |         // Final sync
1175 |         let finalSyncProcess = Process()
1176 |         finalSyncProcess.executableURL = URL(fileURLWithPath: "/bin/sync")
1177 |         try finalSyncProcess.run()
1178 |         finalSyncProcess.waitUntilExit()
1179 |     }
1180 | 
1181 |     // Function to simulate cache pull behavior for freshly downloaded images
1182 |     private func simulateCachePull(tempVMDir: URL) throws {
1183 |         Logger.info("Simulating cache pull behavior for freshly downloaded image...")
1184 | 
1185 |         // Find disk.img in tempVMDir
1186 |         let diskImgPath = tempVMDir.appendingPathComponent("disk.img")
1187 |         guard FileManager.default.fileExists(atPath: diskImgPath.path) else {
1188 |             Logger.info("No disk.img found to simulate cache pull behavior")
1189 |             return
1190 |         }
1191 | 
1192 |         // Get file attributes and size
1193 |         let attributes = try FileManager.default.attributesOfItem(atPath: diskImgPath.path)
1194 |         guard let diskSize = attributes[.size] as? UInt64, diskSize > 0 else {
1195 |             Logger.error("Could not determine disk.img size for simulation")
1196 |             return
1197 |         }
1198 | 
1199 |         Logger.info("Creating true disk image clone with partition table preserved...")
1200 | 
1201 |         // Create backup of original file
1202 |         let backupPath = tempVMDir.appendingPathComponent("disk.img.original")
1203 |         try FileManager.default.moveItem(at: diskImgPath, to: backupPath)
1204 | 
1205 |         // Let's first check if the original image has a partition table
1206 |         Logger.info("Checking if source image has a partition table...")
1207 |         let checkProcess = Process()
1208 |         checkProcess.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
1209 |         checkProcess.arguments = ["imageinfo", backupPath.path]
1210 | 
1211 |         let checkPipe = Pipe()
1212 |         checkProcess.standardOutput = checkPipe
1213 | 
1214 |         try checkProcess.run()
1215 |         checkProcess.waitUntilExit()
1216 | 
1217 |         let checkData = checkPipe.fileHandleForReading.readDataToEndOfFile()
1218 |         let checkOutput = String(data: checkData, encoding: .utf8) ?? ""
1219 |         Logger.info("Source image info: \(checkOutput)")
1220 | 
1221 |         // Try different methods in sequence until one works
1222 |         var success = false
1223 | 
1224 |         // Method 1: Use hdiutil convert to convert the image while preserving all data
1225 |         if !success {
1226 |             Logger.info("Trying hdiutil convert...")
1227 |             let tempPath = tempVMDir.appendingPathComponent("disk.img.temp")
1228 | 
1229 |             let convertProcess = Process()
1230 |             convertProcess.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
1231 |             convertProcess.arguments = [
1232 |                 "convert",
1233 |                 backupPath.path,
1234 |                 "-format", "UDRO",  // Read-only first to preserve partition table
1235 |                 "-o", tempPath.path,
1236 |             ]
1237 | 
1238 |             let convertOutPipe = Pipe()
1239 |             let convertErrPipe = Pipe()
1240 |             convertProcess.standardOutput = convertOutPipe
1241 |             convertProcess.standardError = convertErrPipe
1242 | 
1243 |             do {
1244 |                 try convertProcess.run()
1245 |                 convertProcess.waitUntilExit()
1246 | 
1247 |                 let errData = convertErrPipe.fileHandleForReading.readDataToEndOfFile()
1248 |                 let errOutput = String(data: errData, encoding: .utf8) ?? ""
1249 | 
1250 |                 if convertProcess.terminationStatus == 0 {
1251 |                     Logger.info("hdiutil convert succeeded. Converting to writable format...")
1252 |                     // Now convert to writable format
1253 |                     let convertBackProcess = Process()
1254 |                     convertBackProcess.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
1255 |                     convertBackProcess.arguments = [
1256 |                         "convert",
1257 |                         tempPath.path,
1258 |                         "-format", "UDRW",  // Read-write format
1259 |                         "-o", diskImgPath.path,
1260 |                     ]
1261 | 
1262 |                     try convertBackProcess.run()
1263 |                     convertBackProcess.waitUntilExit()
1264 | 
1265 |                     if convertBackProcess.terminationStatus == 0 {
1266 |                         Logger.info(
1267 |                             "Successfully converted to writable format with partition table")
1268 |                         success = true
1269 |                     } else {
1270 |                         Logger.error("hdiutil convert to writable format failed")
1271 |                     }
1272 | 
1273 |                     // Clean up temporary image
1274 |                     try? FileManager.default.removeItem(at: tempPath)
1275 |                 } else {
1276 |                     Logger.error("hdiutil convert failed: \(errOutput)")
1277 |                 }
1278 |             } catch {
1279 |                 Logger.error("Error executing hdiutil convert: \(error)")
1280 |             }
1281 |         }
1282 | 
1283 |         // Method 2: Try direct raw copy method
1284 |         if !success {
1285 |             Logger.info("Trying direct raw copy with dd...")
1286 | 
1287 |             // Create empty file first
1288 |             FileManager.default.createFile(atPath: diskImgPath.path, contents: nil)
1289 | 
1290 |             let ddProcess = Process()
1291 |             ddProcess.executableURL = URL(fileURLWithPath: "/bin/dd")
1292 |             ddProcess.arguments = [
1293 |                 "if=\(backupPath.path)",
1294 |                 "of=\(diskImgPath.path)",
1295 |                 "bs=1m",  // Large block size
1296 |                 "count=81920",  // Ensure we copy everything (80GB+ should be sufficient)
1297 |             ]
1298 | 
1299 |             let ddErrPipe = Pipe()
1300 |             ddProcess.standardError = ddErrPipe
1301 | 
1302 |             do {
1303 |                 try ddProcess.run()
1304 |                 ddProcess.waitUntilExit()
1305 | 
1306 |                 let errData = ddErrPipe.fileHandleForReading.readDataToEndOfFile()
1307 |                 let errOutput = String(data: errData, encoding: .utf8) ?? ""
1308 | 
1309 |                 if ddProcess.terminationStatus == 0 {
1310 |                     Logger.info("Raw dd copy completed: \(errOutput)")
1311 |                     success = true
1312 |                 } else {
1313 |                     Logger.error("Raw dd copy failed: \(errOutput)")
1314 |                 }
1315 |             } catch {
1316 |                 Logger.error("Error executing dd: \(error)")
1317 |             }
1318 |         }
1319 | 
1320 |         // Method 3: Use a more complex approach with disk mounting
1321 |         if !success {
1322 |             Logger.info("Trying advanced disk attach/detach approach...")
1323 | 
1324 |             // Mount the source disk image
1325 |             let attachProcess = Process()
1326 |             attachProcess.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
1327 |             attachProcess.arguments = ["attach", backupPath.path, "-nomount"]
1328 | 
1329 |             let attachPipe = Pipe()
1330 |             attachProcess.standardOutput = attachPipe
1331 | 
1332 |             try attachProcess.run()
1333 |             attachProcess.waitUntilExit()
1334 | 
1335 |             let attachData = attachPipe.fileHandleForReading.readDataToEndOfFile()
1336 |             let attachOutput = String(data: attachData, encoding: .utf8) ?? ""
1337 | 
1338 |             // Extract the disk device from output (/dev/diskN)
1339 |             var diskDevice: String? = nil
1340 |             if let diskMatch = attachOutput.range(
1341 |                 of: "/dev/disk[0-9]+", options: .regularExpression)
1342 |             {
1343 |                 diskDevice = String(attachOutput[diskMatch])
1344 |             }
1345 | 
1346 |             if let device = diskDevice {
1347 |                 Logger.info("Source disk attached at \(device)")
1348 | 
1349 |                 // Create a bootable disk image clone
1350 |                 let createProcess = Process()
1351 |                 createProcess.executableURL = URL(fileURLWithPath: "/usr/sbin/asr")
1352 |                 createProcess.arguments = [
1353 |                     "restore",
1354 |                     "--source", device,
1355 |                     "--target", diskImgPath.path,
1356 |                     "--erase",
1357 |                     "--noprompt",
1358 |                 ]
1359 | 
1360 |                 let createPipe = Pipe()
1361 |                 createProcess.standardOutput = createPipe
1362 | 
1363 |                 do {
1364 |                     try createProcess.run()
1365 |                     createProcess.waitUntilExit()
1366 | 
1367 |                     let createOutput =
1368 |                         String(
1369 |                             data: createPipe.fileHandleForReading.readDataToEndOfFile(),
1370 |                             encoding: .utf8) ?? ""
1371 |                     Logger.info("asr output: \(createOutput)")
1372 | 
1373 |                     if createProcess.terminationStatus == 0 {
1374 |                         Logger.info("Successfully created bootable disk image clone!")
1375 |                         success = true
1376 |                     } else {
1377 |                         Logger.error("Failed to create bootable disk image clone")
1378 |                     }
1379 |                 } catch {
1380 |                     Logger.error("Error executing asr: \(error)")
1381 |                 }
1382 | 
1383 |                 // Always detach the disk when done
1384 |                 let detachProcess = Process()
1385 |                 detachProcess.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
1386 |                 detachProcess.arguments = ["detach", device]
1387 |                 try? detachProcess.run()
1388 |                 detachProcess.waitUntilExit()
1389 |             } else {
1390 |                 Logger.error("Failed to extract disk device from hdiutil attach output")
1391 |             }
1392 |         }
1393 | 
1394 |         // Fallback: If none of the methods worked, revert to our previous method just to ensure we have a usable image
1395 |         if !success {
1396 |             Logger.info("All specialized methods failed. Reverting to basic copy...")
1397 | 
1398 |             // If the disk image file exists (from a failed attempt), remove it
1399 |             if FileManager.default.fileExists(atPath: diskImgPath.path) {
1400 |                 try FileManager.default.removeItem(at: diskImgPath)
1401 |             }
1402 | 
1403 |             // Attempt a basic file copy which will at least give us something to work with
1404 |             try FileManager.default.copyItem(at: backupPath, to: diskImgPath)
1405 |         }
1406 | 
1407 |         // Optimize sparseness if possible
1408 |         if FileManager.default.fileExists(atPath: "/bin/cp") {
1409 |             Logger.info("Optimizing sparse file representation...")
1410 |             let optimizedPath = diskImgPath.path + ".optimized"
1411 | 
1412 |             let process = Process()
1413 |             process.executableURL = URL(fileURLWithPath: "/bin/cp")
1414 |             process.arguments = ["-c", diskImgPath.path, optimizedPath]
1415 | 
1416 |             try process.run()
1417 |             process.waitUntilExit()
1418 | 
1419 |             if process.terminationStatus == 0 {
1420 |                 let optimizedSize =
1421 |                     (try? FileManager.default.attributesOfItem(atPath: optimizedPath)[.size]
1422 |                         as? UInt64) ?? 0
1423 |                 let originalUsage = getActualDiskUsage(path: diskImgPath.path)
1424 |                 let optimizedUsage = getActualDiskUsage(path: optimizedPath)
1425 | 
1426 |                 Logger.info(
1427 |                     "Sparse optimization results: Before: \(ByteCountFormatter.string(fromByteCount: Int64(originalUsage), countStyle: .file)) actual usage, After: \(ByteCountFormatter.string(fromByteCount: Int64(optimizedUsage), countStyle: .file)) actual usage (Apparent size: \(ByteCountFormatter.string(fromByteCount: Int64(optimizedSize), countStyle: .file)))"
1428 |                 )
1429 | 
1430 |                 // Replace with optimized version
1431 |                 try FileManager.default.removeItem(at: diskImgPath)
1432 |                 try FileManager.default.moveItem(
1433 |                     at: URL(fileURLWithPath: optimizedPath), to: diskImgPath)
1434 |                 Logger.info("Replaced with optimized sparse version")
1435 |             } else {
1436 |                 Logger.info("Sparse optimization failed, using original file")
1437 |                 try? FileManager.default.removeItem(atPath: optimizedPath)
1438 |             }
1439 |         }
1440 | 
1441 |         // Set permissions to 0644
1442 |         let chmodProcess = Process()
1443 |         chmodProcess.executableURL = URL(fileURLWithPath: "/bin/chmod")
1444 |         chmodProcess.arguments = ["0644", diskImgPath.path]
1445 |         try chmodProcess.run()
1446 |         chmodProcess.waitUntilExit()
1447 | 
1448 |         // Final sync
1449 |         let finalSyncProcess = Process()
1450 |         finalSyncProcess.executableURL = URL(fileURLWithPath: "/bin/sync")
1451 |         try finalSyncProcess.run()
1452 |         finalSyncProcess.waitUntilExit()
1453 | 
1454 |         // Verify the final disk image
1455 |         Logger.info("Verifying final disk image partition information...")
1456 |         let verifyProcess = Process()
1457 |         verifyProcess.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil")
1458 |         verifyProcess.arguments = ["imageinfo", diskImgPath.path]
1459 | 
1460 |         let verifyOutputPipe = Pipe()
1461 |         verifyProcess.standardOutput = verifyOutputPipe
1462 | 
1463 |         try verifyProcess.run()
1464 |         verifyProcess.waitUntilExit()
1465 | 
1466 |         let verifyOutputData = verifyOutputPipe.fileHandleForReading.readDataToEndOfFile()
1467 |         let verifyOutput = String(data: verifyOutputData, encoding: .utf8) ?? ""
1468 |         Logger.info("Final disk image verification:\n\(verifyOutput)")
1469 | 
1470 |         // Clean up backup file
1471 |         try FileManager.default.removeItem(at: backupPath)
1472 | 
1473 |         Logger.info(
1474 |             "Cache pull simulation completed successfully with partition table preservation")
1475 |     }
1476 | 
1477 |     private func copyFromCache(manifest: Manifest, manifestId: String, to destination: URL)
1478 |         async throws
1479 |     {
1480 |         Logger.info("Copying from cache...")
1481 | 
1482 |         // Define output URL and expected size variable scope here
1483 |         let outputURL = destination.appendingPathComponent("disk.img")
1484 |         var expectedTotalSize: UInt64? = nil  // Use optional to handle missing config
1485 | 
1486 |         // Define the path for the reassembled cache image
1487 |         let cacheDir = getImageCacheDirectory(manifestId: manifestId)
1488 |         let reassembledCachePath = cacheDir.appendingPathComponent("disk.img.reassembled")
1489 |         let nvramCachePath = cacheDir.appendingPathComponent("nvram.bin")
1490 | 
1491 |         // First check if we already have a reassembled image in the cache
1492 |         if FileManager.default.fileExists(atPath: reassembledCachePath.path) {
1493 |             Logger.info("Found reassembled disk image in cache, using it directly")
1494 | 
1495 |             // Copy reassembled disk image
1496 |             try FileManager.default.copyItem(at: reassembledCachePath, to: outputURL)
1497 | 
1498 |             // Copy nvram if it exists
1499 |             if FileManager.default.fileExists(atPath: nvramCachePath.path) {
1500 |                 try FileManager.default.copyItem(
1501 |                     at: nvramCachePath,
1502 |                     to: destination.appendingPathComponent("nvram.bin")
1503 |                 )
1504 |                 Logger.info("Using cached nvram.bin file")
1505 |             } else {
1506 |                 // Look for nvram in layer cache if needed
1507 |                 let nvramLayers = manifest.layers.filter {
1508 |                     $0.mediaType == "application/octet-stream"
1509 |                 }
1510 |                 if let nvramLayer = nvramLayers.first {
1511 |                     let cachedNvram = getCachedLayerPath(
1512 |                         manifestId: manifestId, digest: nvramLayer.digest)
1513 |                     if FileManager.default.fileExists(atPath: cachedNvram.path) {
1514 |                         try FileManager.default.copyItem(
1515 |                             at: cachedNvram,
1516 |                             to: destination.appendingPathComponent("nvram.bin")
1517 |                         )
1518 |                         // Also save it to the dedicated nvram location for future use
1519 |                         try FileManager.default.copyItem(at: cachedNvram, to: nvramCachePath)
1520 |                         Logger.info("Recovered nvram.bin from layer cache")
1521 |                     }
1522 |                 }
1523 |             }
1524 | 
1525 |             // Copy config if it exists
1526 |             let configCachePath = cacheDir.appendingPathComponent("config.json")
1527 |             if FileManager.default.fileExists(atPath: configCachePath.path) {
1528 |                 try FileManager.default.copyItem(
1529 |                     at: configCachePath,
1530 |                     to: destination.appendingPathComponent("config.json")
1531 |                 )
1532 |                 Logger.info("Using cached config.json file")
1533 |             } else {
1534 |                 // Look for config in layer cache if needed
1535 |                 let configLayers = manifest.layers.filter {
1536 |                     $0.mediaType == "application/vnd.oci.image.config.v1+json"
1537 |                 }
1538 |                 if let configLayer = configLayers.first {
1539 |                     let cachedConfig = getCachedLayerPath(
1540 |                         manifestId: manifestId, digest: configLayer.digest)
1541 |                     if FileManager.default.fileExists(atPath: cachedConfig.path) {
1542 |                         try FileManager.default.copyItem(
1543 |                             at: cachedConfig,
1544 |                             to: destination.appendingPathComponent("config.json")
1545 |                         )
1546 |                         // Also save it to the dedicated config location for future use
1547 |                         try FileManager.default.copyItem(at: cachedConfig, to: configCachePath)
1548 |                         Logger.info("Recovered config.json from layer cache")
1549 |                     }
1550 |                 }
1551 |             }
1552 | 
1553 |             Logger.info("Cache copy complete using reassembled image")
1554 |             return
1555 |         }
1556 | 
1557 |         // If we don't have a reassembled image, proceed with legacy part handling
1558 |         Logger.info("No reassembled image found, using part-based reassembly")
1559 | 
1560 |         // Instantiate collector
1561 |         let diskPartsCollector = DiskPartsCollector()
1562 |         var lz4LayerCount = 0  // Count lz4 layers found
1563 |         var hasNvram = false
1564 |         var configPath: URL? = nil
1565 | 
1566 |         // First identify disk parts and non-disk files
1567 |         for layer in manifest.layers {
1568 |             let cachedLayer = getCachedLayerPath(manifestId: manifestId, digest: layer.digest)
1569 | 
1570 |             // Identify disk parts simply by media type
1571 |             if layer.mediaType == "application/octet-stream+lz4" {
1572 |                 lz4LayerCount += 1  // Increment count
1573 | 
1574 |                 // When caching is disabled, the file might not exist with the cache path name
1575 |                 // Check if the file exists before trying to use it
1576 |                 if !FileManager.default.fileExists(atPath: cachedLayer.path) {
1577 |                     Logger.info("Layer file not found in cache: \(cachedLayer.path) - skipping")
1578 |                     continue
1579 |                 }
1580 | 
1581 |                 // Add to collector. It will assign the sequential part number.
1582 |                 let collectorPartNum = await diskPartsCollector.addPart(url: cachedLayer)
1583 |                 Logger.info(
1584 |                     "Adding cached lz4 layer (part \(lz4LayerCount)) -> Collector #\(collectorPartNum): \(cachedLayer.lastPathComponent)"
1585 |                 )
1586 |             } else {
1587 |                 // --- Handle Non-Disk-Part Layer (from cache) ---
1588 |                 let fileName: String
1589 |                 switch layer.mediaType {
1590 |                 case "application/vnd.oci.image.config.v1+json":
1591 |                     fileName = "config.json"
1592 |                     configPath = cachedLayer
1593 |                 case "application/octet-stream":
1594 |                     // Assume nvram if config layer exists, otherwise assume single disk image
1595 |                     fileName = manifest.config != nil ? "nvram.bin" : "disk.img"
1596 |                     if fileName == "nvram.bin" {
1597 |                         hasNvram = true
1598 |                     }
1599 |                 case "application/vnd.oci.image.layer.v1.tar",
1600 |                     "application/octet-stream+gzip":
1601 |                     // Assume disk image for these types as well if encountered in cache scenario
1602 |                     fileName = "disk.img"
1603 |                 default:
1604 |                     Logger.info("Skipping unsupported cached layer media type: \(layer.mediaType)")
1605 |                     continue
1606 |                 }
1607 | 
1608 |                 // When caching is disabled, the file might not exist with the cache path name
1609 |                 if !FileManager.default.fileExists(atPath: cachedLayer.path) {
1610 |                     Logger.info(
1611 |                         "Non-disk layer file not found in cache: \(cachedLayer.path) - skipping")
1612 |                     continue
1613 |                 }
1614 | 
1615 |                 // Copy the non-disk file directly from cache to destination
1616 |                 try FileManager.default.copyItem(
1617 |                     at: cachedLayer,
1618 |                     to: destination.appendingPathComponent(fileName)
1619 |                 )
1620 |             }
1621 |         }
1622 | 
1623 |         // --- Safely retrieve parts AFTER loop ---
1624 |         let diskPartSources = await diskPartsCollector.getSortedParts()  // Sorted by assigned sequential number
1625 |         let totalParts = await diskPartsCollector.getTotalParts()  // Get total count from collector
1626 | 
1627 |         Logger.info("Found \(totalParts) lz4 disk parts in cache to reassemble.")
1628 |         // --- End retrieving parts ---
1629 | 
1630 |         // Reassemble disk parts if needed
1631 |         // Use the count from the collector
1632 |         if !diskPartSources.isEmpty {
1633 |             // Use totalParts from collector directly
1634 |             Logger.info(
1635 |                 "Reassembling \(totalParts) disk image parts using sparse file technique...")
1636 | 
1637 |             // Get uncompressed size from cached config file (needs to be copied first)
1638 |             let configURL = destination.appendingPathComponent("config.json")
1639 |             // Parse config.json to get uncompressed size *before* reassembly
1640 |             let uncompressedSize = getUncompressedSizeFromConfig(configPath: configURL)
1641 | 
1642 |             // Now also try to get disk size from VM config if OCI annotation not found
1643 |             var vmConfigDiskSize: UInt64? = nil
1644 |             if uncompressedSize == nil && FileManager.default.fileExists(atPath: configURL.path) {
1645 |                 do {
1646 |                     let configData = try Data(contentsOf: configURL)
1647 |                     let decoder = JSONDecoder()
1648 |                     if let vmConfig = try? decoder.decode(VMConfig.self, from: configData) {
1649 |                         vmConfigDiskSize = vmConfig.diskSize
1650 |                         if let size = vmConfigDiskSize {
1651 |                             Logger.info("Found diskSize from VM config.json: \(size) bytes")
1652 |                         }
1653 |                     }
1654 |                 } catch {
1655 |                     Logger.error("Failed to parse VM config.json for diskSize: \(error)")
1656 |                 }
1657 |             }
1658 | 
1659 |             // Determine the size to use for the sparse file
1660 |             // Use: annotation size > VM config diskSize > fallback (error)
1661 |             if let size = uncompressedSize {
1662 |                 Logger.info("Using uncompressed size from annotation: \(size) bytes")
1663 |                 expectedTotalSize = size
1664 |             } else if let size = vmConfigDiskSize {
1665 |                 Logger.info("Using diskSize from VM config: \(size) bytes")
1666 |                 expectedTotalSize = size
1667 |             } else {
1668 |                 // If neither is found in cache scenario, throw error as we cannot determine the size
1669 |                 Logger.error(
1670 |                     "Missing both uncompressed size annotation and VM config diskSize for cached multi-part image."
1671 |                         + " Cannot reassemble."
1672 |                 )
1673 |                 throw PullError.missingUncompressedSizeAnnotation
1674 |             }
1675 | 
1676 |             // Now that expectedTotalSize is guaranteed to be non-nil, proceed with setup
1677 |             guard let sizeForTruncate = expectedTotalSize else {
1678 |                 // This should not happen due to the checks above, but safety first
1679 |                 let nilError: Error? = nil
1680 |                 // Use nil-coalescing to provide a default error, appeasing the compiler
1681 |                 throw PullError.reassemblySetupFailed(
1682 |                     path: outputURL.path, underlyingError: nilError ?? NoSpecificUnderlyingError())
1683 |             }
1684 | 
1685 |             // If we have just one disk part, use the shared function
1686 |             if totalParts == 1 {
1687 |                 // Single part - use shared function
1688 |                 let sourceURL = diskPartSources[0].1  // Get the first source URL (index 1 of the tuple)
1689 |                 try createDiskImageFromSource(
1690 |                     sourceURL: sourceURL,
1691 |                     destinationURL: outputURL,
1692 |                     diskSize: sizeForTruncate
1693 |                 )
1694 |             } else {
1695 |                 // Multiple parts - we need to reassemble
1696 |                 // Wrap file handle setup and sparse file creation within this block
1697 |                 let outputHandle: FileHandle
1698 |                 do {
1699 |                     // Ensure parent directory exists
1700 |                     try FileManager.default.createDirectory(
1701 |                         at: outputURL.deletingLastPathComponent(), withIntermediateDirectories: true
1702 |                     )
1703 |                     // Explicitly create the file first, removing old one if needed
1704 |                     if FileManager.default.fileExists(atPath: outputURL.path) {
1705 |                         try FileManager.default.removeItem(at: outputURL)
1706 |                     }
1707 |                     guard FileManager.default.createFile(atPath: outputURL.path, contents: nil)
1708 |                     else {
1709 |                         throw PullError.fileCreationFailed(outputURL.path)
1710 |                     }
1711 |                     // Open handle for writing
1712 |                     outputHandle = try FileHandle(forWritingTo: outputURL)
1713 |                     // Set the file size (creates sparse file)
1714 |                     try outputHandle.truncate(atOffset: sizeForTruncate)
1715 |                     Logger.info(
1716 |                         "Sparse file initialized for cache reassembly with size: \(ByteCountFormatter.string(fromByteCount: Int64(sizeForTruncate), countStyle: .file))"
1717 |                     )
1718 |                 } catch {
1719 |                     Logger.error(
1720 |                         "Failed during setup for cached disk image reassembly: \(error.localizedDescription)",
1721 |                         metadata: ["path": outputURL.path])
1722 |                     throw PullError.reassemblySetupFailed(
1723 |                         path: outputURL.path, underlyingError: error)
1724 |                 }
1725 | 
1726 |                 // Ensure handle is closed when exiting this scope
1727 |                 defer { try? outputHandle.close() }
1728 | 
1729 |                 var reassemblyProgressLogger = ProgressLogger(threshold: 0.05)
1730 |                 var currentOffset: UInt64 = 0
1731 | 
1732 |                 // Iterate from 1 up to the total number of parts found by the collector
1733 |                 for collectorPartNum in 1...totalParts {
1734 |                     // Find the source URL from our collected parts using the sequential collectorPartNum
1735 |                     guard
1736 |                         let sourceInfo = diskPartSources.first(where: { $0.0 == collectorPartNum })
1737 |                     else {
1738 |                         Logger.error(
1739 |                             "Missing required cached part number \(collectorPartNum) in collected parts during reassembly."
1740 |                         )
1741 |                         throw PullError.missingPart(collectorPartNum)
1742 |                     }
1743 |                     let sourceURL = sourceInfo.1  // Get URL from tuple
1744 | 
1745 |                     // Log using the sequential collector part number
1746 |                     Logger.info(
1747 |                         "Decompressing part \(collectorPartNum) of \(totalParts) from cache: \(sourceURL.lastPathComponent) at offset \(currentOffset)..."
1748 |                     )
1749 | 
1750 |                     // Always use the correct sparse decompression function
1751 |                     let decompressedBytesWritten = try decompressChunkAndWriteSparse(
1752 |                         inputPath: sourceURL.path,
1753 |                         outputHandle: outputHandle,
1754 |                         startOffset: currentOffset
1755 |                     )
1756 |                     currentOffset += decompressedBytesWritten
1757 |                     // Update progress (using sizeForTruncate which should be available)
1758 |                     reassemblyProgressLogger.logProgress(
1759 |                         current: Double(currentOffset) / Double(sizeForTruncate),
1760 |                         context: "Reassembling Cache")
1761 | 
1762 |                     try outputHandle.synchronize()  // Explicitly synchronize after each chunk
1763 |                 }
1764 | 
1765 |                 // Finalize progress, close handle (done by defer)
1766 |                 reassemblyProgressLogger.logProgress(current: 1.0, context: "Reassembly Complete")
1767 | 
1768 |                 // Add test patterns at the beginning and end of the file
1769 |                 Logger.info("Writing test patterns to sparse file to verify integrity...")
1770 |                 let testPattern = "LUME_TEST_PATTERN".data(using: .utf8)!
1771 |                 try outputHandle.seek(toOffset: 0)
1772 |                 try outputHandle.write(contentsOf: testPattern)
1773 |                 try outputHandle.seek(toOffset: sizeForTruncate - UInt64(testPattern.count))
1774 |                 try outputHandle.write(contentsOf: testPattern)
1775 |                 try outputHandle.synchronize()
1776 | 
1777 |                 // Ensure handle is properly synchronized before closing
1778 |                 try outputHandle.synchronize()
1779 | 
1780 |                 // Close handle explicitly instead of relying on defer
1781 |                 try outputHandle.close()
1782 | 
1783 |                 // Verify final size
1784 |                 let finalSize =
1785 |                     (try? FileManager.default.attributesOfItem(atPath: outputURL.path)[.size]
1786 |                         as? UInt64) ?? 0
1787 |                 Logger.info(
1788 |                     "Final disk image size from cache (before sparse file optimization): \(ByteCountFormatter.string(fromByteCount: Int64(finalSize), countStyle: .file))"
1789 |                 )
1790 | 
1791 |                 // Use the calculated sizeForTruncate for comparison
1792 |                 if finalSize != sizeForTruncate {
1793 |                     Logger.info(
1794 |                         "Warning: Final reported size (\(finalSize) bytes) differs from expected size (\(sizeForTruncate) bytes), but this doesn't affect functionality"
1795 |                     )
1796 |                 }
1797 | 
1798 |                 Logger.info("Disk image reassembly completed")
1799 | 
1800 |                 // Optimize sparseness for cached reassembly if on macOS
1801 |                 if FileManager.default.fileExists(atPath: "/bin/cp") {
1802 |                     Logger.info("Optimizing sparse file representation for cached reassembly...")
1803 |                     let optimizedPath = outputURL.path + ".optimized"
1804 | 
1805 |                     let process = Process()
1806 |                     process.executableURL = URL(fileURLWithPath: "/bin/cp")
1807 |                     process.arguments = ["-c", outputURL.path, optimizedPath]
1808 | 
1809 |                     do {
1810 |                         try process.run()
1811 |                         process.waitUntilExit()
1812 | 
1813 |                         if process.terminationStatus == 0 {
1814 |                             // Get size of optimized file
1815 |                             let optimizedSize =
1816 |                                 (try? FileManager.default.attributesOfItem(atPath: optimizedPath)[
1817 |                                     .size] as? UInt64) ?? 0
1818 |                             let originalUsage = getActualDiskUsage(path: outputURL.path)
1819 |                             let optimizedUsage = getActualDiskUsage(path: optimizedPath)
1820 | 
1821 |                             Logger.info(
1822 |                                 "Sparse optimization results for cache: Before: \(ByteCountFormatter.string(fromByteCount: Int64(originalUsage), countStyle: .file)) actual usage, After: \(ByteCountFormatter.string(fromByteCount: Int64(optimizedUsage), countStyle: .file)) actual usage (Apparent size: \(ByteCountFormatter.string(fromByteCount: Int64(optimizedSize), countStyle: .file)))"
1823 |                             )
1824 | 
1825 |                             // Replace the original with the optimized version
1826 |                             try FileManager.default.removeItem(at: outputURL)
1827 |                             try FileManager.default.moveItem(
1828 |                                 at: URL(fileURLWithPath: optimizedPath), to: outputURL)
1829 |                             Logger.info("Replaced cached reassembly with optimized sparse version")
1830 |                         } else {
1831 |                             Logger.info("Sparse optimization failed for cache, using original file")
1832 |                             try? FileManager.default.removeItem(atPath: optimizedPath)
1833 |                         }
1834 |                     } catch {
1835 |                         Logger.info(
1836 |                             "Error during sparse optimization for cache: \(error.localizedDescription)"
1837 |                         )
1838 |                         try? FileManager.default.removeItem(atPath: optimizedPath)
1839 |                     }
1840 |                 }
1841 | 
1842 |                 // Set permissions to ensure consistency
1843 |                 let chmodProcess = Process()
1844 |                 chmodProcess.executableURL = URL(fileURLWithPath: "/bin/chmod")
1845 |                 chmodProcess.arguments = ["0644", outputURL.path]
1846 |                 try chmodProcess.run()
1847 |                 chmodProcess.waitUntilExit()
1848 |             }
1849 | 
1850 |             // After successful reassembly, store the reassembled image in the cache
1851 |             if cachingEnabled {
1852 |                 Logger.info("Saving reassembled disk image to cache for future use")
1853 | 
1854 |                 // Copy the reassembled disk image to the cache
1855 |                 try FileManager.default.copyItem(at: outputURL, to: reassembledCachePath)
1856 | 
1857 |                 // Clean up disk parts after successful reassembly
1858 |                 Logger.info("Cleaning up disk part files from cache")
1859 | 
1860 |                 // Use an array to track unique file paths to avoid trying to delete the same file multiple times
1861 |                 var processedPaths: [String] = []
1862 | 
1863 |                 for (_, partURL) in diskPartSources {
1864 |                     let path = partURL.path
1865 | 
1866 |                     // Skip if we've already processed this exact path
1867 |                     if processedPaths.contains(path) {
1868 |                         Logger.info("Skipping duplicate part file: \(partURL.lastPathComponent)")
1869 |                         continue
1870 |                     }
1871 | 
1872 |                     // Add to processed array
1873 |                     processedPaths.append(path)
1874 | 
1875 |                     // Check if file exists before attempting to delete
1876 |                     if FileManager.default.fileExists(atPath: path) {
1877 |                         do {
1878 |                             try FileManager.default.removeItem(at: partURL)
1879 |                             Logger.info("Removed disk part: \(partURL.lastPathComponent)")
1880 |                         } catch {
1881 |                             Logger.info(
1882 |                                 "Failed to remove disk part: \(partURL.lastPathComponent) - \(error.localizedDescription)"
1883 |                             )
1884 |                         }
1885 |                     } else {
1886 |                         Logger.info("Disk part already removed: \(partURL.lastPathComponent)")
1887 |                     }
1888 |                 }
1889 | 
1890 |                 // Also save nvram if we have it
1891 |                 if hasNvram {
1892 |                     let srcNvram = destination.appendingPathComponent("nvram.bin")
1893 |                     if FileManager.default.fileExists(atPath: srcNvram.path) {
1894 |                         try? FileManager.default.copyItem(at: srcNvram, to: nvramCachePath)
1895 |                     }
1896 |                 }
1897 | 
1898 |                 // Save config.json in the cache for future use if it exists
1899 |                 if let configPath = configPath {
1900 |                     let cacheConfigPath = cacheDir.appendingPathComponent("config.json")
1901 |                     try? FileManager.default.copyItem(at: configPath, to: cacheConfigPath)
1902 |                 }
1903 | 
1904 |                 // Perform a final cleanup to catch any leftover part files
1905 |                 Logger.info("Performing final cleanup of any remaining part files")
1906 |                 do {
1907 |                     let cacheContents = try FileManager.default.contentsOfDirectory(
1908 |                         at: cacheDir, includingPropertiesForKeys: nil)
1909 | 
1910 |                     for item in cacheContents {
1911 |                         let fileName = item.lastPathComponent
1912 |                         // Only remove sha256_ files that aren't the reassembled image, nvram or config
1913 |                         if fileName.starts(with: "sha256_") && fileName != "disk.img.reassembled"
1914 |                             && fileName != "nvram.bin" && fileName != "config.json"
1915 |                             && fileName != "manifest.json" && fileName != "metadata.json"
1916 |                         {
1917 |                             do {
1918 |                                 try FileManager.default.removeItem(at: item)
1919 |                                 Logger.info(
1920 |                                     "Removed leftover file during final cleanup: \(fileName)")
1921 |                             } catch {
1922 |                                 Logger.info(
1923 |                                     "Failed to remove leftover file: \(fileName) - \(error.localizedDescription)"
1924 |                                 )
1925 |                             }
1926 |                         }
1927 |                     }
1928 |                 } catch {
1929 |                     Logger.info("Error during final cleanup: \(error.localizedDescription)")
1930 |                 }
1931 |             }
1932 |         }
1933 | 
1934 |         Logger.info("Cache copy complete")
1935 |     }
1936 | 
1937 |     private func getToken(
1938 |         repository: String, scopes: [String] = ["pull"], requireAllScopes: Bool = false
1939 |     ) async throws
1940 |         -> String
1941 |     {
1942 |         let encodedRepo =
1943 |             repository.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? repository
1944 | 
1945 |         // Build scope string from scopes array
1946 |         let scopeString = scopes.joined(separator: ",")
1947 | 
1948 |         Logger.info("Requesting token with scopes: \(scopeString) for repository: \(repository)")
1949 | 
1950 |         // Request both pull and push scope for uploads
1951 |         let url = URL(
1952 |             string:
1953 |                 "https://\(self.registry)/token?scope=repository:\(encodedRepo):\(scopeString)&service=\(self.registry)"
1954 |         )!
1955 | 
1956 |         var request = URLRequest(url: url)
1957 |         request.httpMethod = "GET"  // Token endpoint uses GET
1958 |         request.setValue("application/json", forHTTPHeaderField: "Accept")
1959 | 
1960 |         // *** Add Basic Authentication Header if credentials exist ***
1961 |         let (username, password) = getCredentialsFromEnvironment()
1962 |         if let username = username, let password = password, !username.isEmpty, !password.isEmpty {
1963 |             let authString = "\(username):\(password)"
1964 |             if let authData = authString.data(using: .utf8) {
1965 |                 let base64Auth = authData.base64EncodedString()
1966 |                 request.setValue("Basic \(base64Auth)", forHTTPHeaderField: "Authorization")
1967 |                 Logger.info("Adding Basic Authentication header to token request")
1968 |             } else {
1969 |                 Logger.error("Failed to encode credentials for Basic Auth")
1970 |             }
1971 |         } else {
1972 |             Logger.info("No credentials found in environment for token request")
1973 |             // Allow anonymous request for pull scope, but push scope likely requires auth
1974 |         }
1975 |         // *** End Basic Auth addition ***
1976 | 
1977 |         let (data, response) = try await URLSession.shared.data(for: request)
1978 | 
1979 |         // Check response status code *before* parsing JSON
1980 |         guard let httpResponse = response as? HTTPURLResponse else {
1981 |             throw PushError.authenticationFailed  // Or a more generic network error
1982 |         }
1983 | 
1984 |         // Handle errors based on status code
1985 |         if httpResponse.statusCode != 200 {
1986 |             // Special handling for push operations that require all scopes
1987 |             if requireAllScopes
1988 |                 && (httpResponse.statusCode == 401 || httpResponse.statusCode == 403)
1989 |             {
1990 |                 // Try to parse the error message from the response
1991 |                 let errorResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
1992 |                 let errors = errorResponse?["errors"] as? [[String: Any]]
1993 |                 let errorMessage = errors?.first?["message"] as? String ?? "Permission denied"
1994 | 
1995 |                 Logger.error("Push permission denied: \(errorMessage)")
1996 |                 Logger.error(
1997 |                     "Your token does not have 'packages:write' permission to \(repository)")
1998 |                 Logger.error("Make sure you have the appropriate access rights to the repository")
1999 | 
2000 |                 // Check if this is an organization repository
2001 |                 if repository.contains("/") {
2002 |                     let orgName = repository.split(separator: "/").first.map(String.init) ?? ""
2003 |                     if orgName != "" {
2004 |                         Logger.error("For organization repositories (\(orgName)), you must:")
2005 |                         Logger.error("1. Be a member of the organization with write access")
2006 |                         Logger.error("2. Have a token with 'write:packages' scope")
2007 |                         Logger.error(
2008 |                             "3. The organization must allow you to create/publish packages")
2009 |                     }
2010 |                 }
2011 | 
2012 |                 throw PushError.insufficientPermissions("Push permission denied: \(errorMessage)")
2013 |             }
2014 | 
2015 |             // If we get 403 and we're requesting both pull and push, retry with just pull
2016 |             // ONLY if requireAllScopes is false
2017 |             if httpResponse.statusCode == 403 && scopes.contains("push")
2018 |                 && scopes.contains("pull") && !requireAllScopes
2019 |             {
2020 |                 Logger.info("Permission denied for push scope, retrying with pull scope only")
2021 |                 return try await getToken(repository: repository, scopes: ["pull"])
2022 |             }
2023 | 
2024 |             // For pull scope only, if authentication fails, assume this is a public image
2025 |             // and continue without a token (empty string)
2026 |             if scopes == ["pull"] {
2027 |                 Logger.info(
2028 |                     "Authentication failed for pull scope, assuming public image and continuing without token"
2029 |                 )
2030 |                 return ""
2031 |             }
2032 | 
2033 |             // Handle other authentication failures
2034 |             let responseBody = String(data: data, encoding: .utf8) ?? "(Could not decode body)"
2035 |             Logger.error(
2036 |                 "Token request failed with status code: \(httpResponse.statusCode). Response: \(responseBody)"
2037 |             )
2038 | 
2039 |             throw PushError.authenticationFailed
2040 |         }
2041 | 
2042 |         let jsonResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any]
2043 |         guard
2044 |             let token = jsonResponse?["token"] as? String ?? jsonResponse?["access_token"]
2045 |                 as? String
2046 |         else {
2047 |             Logger.error("Token not found in registry response")
2048 |             throw PushError.missingToken
2049 |         }
2050 | 
2051 |         return token
2052 |     }
2053 | 
2054 |     private func fetchManifest(repository: String, tag: String, token: String) async throws -> (
2055 |         Manifest, String
2056 |     ) {
2057 |         var request = URLRequest(
2058 |             url: URL(string: "https://\(self.registry)/v2/\(repository)/manifests/\(tag)")!)
2059 | 
2060 |         // Only add Authorization header if token is not empty
2061 |         if !token.isEmpty {
2062 |             request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
2063 |         }
2064 | 
2065 |         request.addValue("application/vnd.oci.image.manifest.v1+json", forHTTPHeaderField: "Accept")
2066 | 
2067 |         let (data, response) = try await URLSession.shared.data(for: request)
2068 |         guard let httpResponse = response as? HTTPURLResponse,
2069 |             httpResponse.statusCode == 200,
2070 |             let digest = httpResponse.value(forHTTPHeaderField: "Docker-Content-Digest")
2071 |         else {
2072 |             throw PullError.manifestFetchFailed
2073 |         }
2074 | 
2075 |         let manifest = try JSONDecoder().decode(Manifest.self, from: data)
2076 |         return (manifest, digest)
2077 |     }
2078 | 
2079 |     private func downloadLayer(
2080 |         repository: String,
2081 |         digest: String,
2082 |         mediaType: String,
2083 |         token: String,
2084 |         to url: URL,
2085 |         maxRetries: Int = 5,
2086 |         progress: isolated ProgressTracker,
2087 |         manifestId: String? = nil
2088 |     ) async throws {
2089 |         var lastError: Error?
2090 | 
2091 |         // Create a shared session configuration for all download attempts
2092 |         let config = URLSessionConfiguration.default
2093 |         config.timeoutIntervalForRequest = 60
2094 |         config.timeoutIntervalForResource = 3600
2095 |         config.waitsForConnectivity = true
2096 |         config.httpMaximumConnectionsPerHost = 6
2097 |         config.httpShouldUsePipelining = true
2098 |         config.requestCachePolicy = .reloadIgnoringLocalCacheData
2099 | 
2100 |         // Enable HTTP/2 when available
2101 |         if #available(macOS 13.0, *) {
2102 |             config.httpAdditionalHeaders = ["Connection": "keep-alive"]
2103 |         }
2104 | 
2105 |         // Check for TCP window size and optimize if possible
2106 |         if getTCPReceiveWindowSize() != nil {
2107 |             config.networkServiceType = .responsiveData
2108 |         }
2109 | 
2110 |         // Create one session to be reused across retries
2111 |         let session = URLSession(configuration: config)
2112 | 
2113 |         for attempt in 1...maxRetries {
2114 |             do {
2115 |                 var request = URLRequest(
2116 |                     url: URL(string: "https://\(self.registry)/v2/\(repository)/blobs/\(digest)")!)
2117 | 
2118 |                 // Only add Authorization header if token is not empty
2119 |                 if !token.isEmpty {
2120 |                     request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
2121 |                 }
2122 | 
2123 |                 request.addValue(mediaType, forHTTPHeaderField: "Accept")
2124 |                 request.timeoutInterval = 60
2125 | 
2126 |                 // Add Accept-Encoding for compressed transfer if content isn't already compressed
2127 |                 if !mediaType.contains("gzip") && !mediaType.contains("compressed") {
2128 |                     request.addValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")
2129 |                 }
2130 | 
2131 |                 let (tempURL, response) = try await session.download(for: request)
2132 |                 guard let httpResponse = response as? HTTPURLResponse,
2133 |                     httpResponse.statusCode == 200
2134 |                 else {
2135 |                     throw PullError.layerDownloadFailed(digest)
2136 |                 }
2137 | 
2138 |                 try FileManager.default.createDirectory(
2139 |                     at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
2140 |                 try FileManager.default.moveItem(at: tempURL, to: url)
2141 |                 progress.addProgress(Int64(httpResponse.expectedContentLength))
2142 | 
2143 |                 // Always save a copy to the cache directory for use by copyFromCache,
2144 |                 // even if caching is disabled
2145 |                 if let manifestId = manifestId {
2146 |                     let cachedLayer = getCachedLayerPath(manifestId: manifestId, digest: digest)
2147 |                     // Make sure cache directory exists
2148 |                     try FileManager.default.createDirectory(
2149 |                         at: cachedLayer.deletingLastPathComponent(),
2150 |                         withIntermediateDirectories: true
2151 |                     )
2152 | 
2153 |                     if FileManager.default.fileExists(atPath: cachedLayer.path) {
2154 |                         try FileManager.default.removeItem(at: cachedLayer)
2155 |                     }
2156 |                     try FileManager.default.copyItem(at: url, to: cachedLayer)
2157 |                 }
2158 | 
2159 |                 // Mark download as complete regardless of caching
2160 |                 markDownloadComplete(digest)
2161 |                 return
2162 | 
2163 |             } catch {
2164 |                 lastError = error
2165 |                 if attempt < maxRetries {
2166 |                     // Exponential backoff with jitter for retries
2167 |                     let baseDelay = Double(attempt) * 2
2168 |                     let jitter = Double.random(in: 0...1)
2169 |                     let delay = baseDelay + jitter
2170 |                     try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
2171 | 
2172 |                     Logger.info("Retrying download (attempt \(attempt+1)/\(maxRetries)): \(digest)")
2173 |                 }
2174 |             }
2175 |         }
2176 | 
2177 |         throw lastError ?? PullError.layerDownloadFailed(digest)
2178 |     }
2179 | 
2180 |     // Function removed as it's not applicable to the observed manifest format
2181 |     /*
2182 |     private func extractPartInfo(from mediaType: String) -> (partNum: Int, total: Int)? {
2183 |         let pattern = #"part\\.number=(\\d+);part\\.total=(\\d+)"#
2184 |         guard let regex = try? NSRegularExpression(pattern: pattern),
2185 |             let match = regex.firstMatch(
2186 |                 in: mediaType,
2187 |                 range: NSRange(mediaType.startIndex..., in: mediaType)
2188 |             ),
2189 |             let partNumRange = Range(match.range(at: 1), in: mediaType),
2190 |             let totalRange = Range(match.range(at: 2), in: mediaType),
2191 |             let partNum = Int(mediaType[partNumRange]),
2192 |             let total = Int(mediaType[totalRange])
2193 |         else {
2194 |             return nil
2195 |         }
2196 |         return (partNum, total)
2197 |     }
2198 |     */
2199 | 
2200 |     private func listRepositories() async throws -> [String] {
2201 |         var request = URLRequest(
2202 |             url: URL(string: "https://\(registry)/v2/\(organization)/repositories/list")!)
2203 |         request.setValue("application/json", forHTTPHeaderField: "Accept")
2204 | 
2205 |         let (data, response) = try await URLSession.shared.data(for: request)
2206 |         guard let httpResponse = response as? HTTPURLResponse else {
2207 |             throw PullError.manifestFetchFailed
2208 |         }
2209 | 
2210 |         if httpResponse.statusCode == 404 {
2211 |             return []
2212 |         }
2213 | 
2214 |         guard httpResponse.statusCode == 200 else {
2215 |             throw PullError.manifestFetchFailed
2216 |         }
2217 | 
2218 |         let repoList = try JSONDecoder().decode(RepositoryList.self, from: data)
2219 |         return repoList.repositories
2220 |     }
2221 | 
2222 |     func getImages() async throws -> [CachedImage] {
2223 |         Logger.info("Scanning for cached images in \(cacheDirectory.path)")
2224 |         var images: [CachedImage] = []
2225 |         let orgDir = cacheDirectory.appendingPathComponent(organization)
2226 | 
2227 |         if FileManager.default.fileExists(atPath: orgDir.path) {
2228 |             let contents = try FileManager.default.contentsOfDirectory(atPath: orgDir.path)
2229 |             Logger.info("Found \(contents.count) items in cache directory")
2230 | 
2231 |             for item in contents {
2232 |                 let itemPath = orgDir.appendingPathComponent(item)
2233 |                 var isDirectory: ObjCBool = false
2234 | 
2235 |                 guard
2236 |                     FileManager.default.fileExists(
2237 |                         atPath: itemPath.path, isDirectory: &isDirectory),
2238 |                     isDirectory.boolValue
2239 |                 else { continue }
2240 | 
2241 |                 // First try to read metadata file
2242 |                 let metadataPath = itemPath.appendingPathComponent("metadata.json")
2243 |                 if let metadataData = try? Data(contentsOf: metadataPath),
2244 |                     let metadata = try? JSONDecoder().decode(ImageMetadata.self, from: metadataData)
2245 |                 {
2246 |                     Logger.info(
2247 |                         "Found metadata for image",
2248 |                         metadata: [
2249 |                             "image": metadata.image,
2250 |                             "manifest_id": metadata.manifestId,
2251 |                         ])
2252 |                     images.append(
2253 |                         CachedImage(
2254 |                             repository: metadata.image,
2255 |                             imageId: String(metadata.manifestId.prefix(12)),
2256 |                             manifestId: metadata.manifestId
2257 |                         ))
2258 |                     continue
2259 |                 }
2260 | 
2261 |                 // Fallback to checking manifest if metadata doesn't exist
2262 |                 Logger.info("No metadata found for \(item), checking manifest")
2263 |                 let manifestPath = itemPath.appendingPathComponent("manifest.json")
2264 |                 guard FileManager.default.fileExists(atPath: manifestPath.path),
2265 |                     let manifestData = try? Data(contentsOf: manifestPath),
2266 |                     let manifest = try? JSONDecoder().decode(Manifest.self, from: manifestData)
2267 |                 else {
2268 |                     Logger.info("No valid manifest found for \(item)")
2269 |                     continue
2270 |                 }
2271 | 
2272 |                 let manifestId = item
2273 | 
2274 |                 // Verify the manifest ID matches
2275 |                 let currentManifestId = getManifestIdentifier(manifest, manifestDigest: "")
2276 |                 Logger.info(
2277 |                     "Manifest check",
2278 |                     metadata: [
2279 |                         "item": item,
2280 |                         "current_manifest_id": currentManifestId,
2281 |                         "matches": "\(currentManifestId == manifestId)",
2282 |                     ])
2283 |                 if currentManifestId == manifestId {
2284 |                     // Skip if we can't determine the repository name
2285 |                     // This should be rare since we now save metadata during pull
2286 |                     Logger.info("Skipping image without metadata: \(item)")
2287 |                     continue
2288 |                 }
2289 |             }
2290 |         } else {
2291 |             Logger.info("Cache directory does not exist")
2292 |         }
2293 | 
2294 |         Logger.info("Found \(images.count) cached images")
2295 |         return images.sorted {
2296 |             $0.repository == $1.repository ? $0.imageId < $1.imageId : $0.repository < $1.repository
2297 |         }
2298 |     }
2299 | 
2300 |     private func listRemoteImageTags(repository: String) async throws -> [String] {
2301 |         var request = URLRequest(
2302 |             url: URL(string: "https://\(registry)/v2/\(organization)/\(repository)/tags/list")!)
2303 |         request.setValue("application/json", forHTTPHeaderField: "Accept")
2304 | 
2305 |         let (data, response) = try await URLSession.shared.data(for: request)
2306 |         guard let httpResponse = response as? HTTPURLResponse else {
2307 |             throw PullError.manifestFetchFailed
2308 |         }
2309 | 
2310 |         if httpResponse.statusCode == 404 {
2311 |             return []
2312 |         }
2313 | 
2314 |         guard httpResponse.statusCode == 200 else {
2315 |             throw PullError.manifestFetchFailed
2316 |         }
2317 | 
2318 |         let repoTags = try JSONDecoder().decode(RepositoryTags.self, from: data)
2319 |         return repoTags.tags
2320 |     }
2321 | 
2322 |     // Determine appropriate chunk size based on available system memory on macOS
2323 |     private func getOptimalChunkSize() -> Int {
2324 |         // Try to get system memory info
2325 |         var stats = vm_statistics64_data_t()
2326 |         var size = mach_msg_type_number_t(
2327 |             MemoryLayout<vm_statistics64_data_t>.size / MemoryLayout<integer_t>.size)
2328 |         let hostPort = mach_host_self()
2329 | 
2330 |         let result = withUnsafeMutablePointer(to: &stats) { statsPtr in
2331 |             statsPtr.withMemoryRebound(to: integer_t.self, capacity: Int(size)) { ptr in
2332 |                 host_statistics64(hostPort, HOST_VM_INFO64, ptr, &size)
2333 |             }
2334 |         }
2335 | 
2336 |         // Define chunk size parameters
2337 |         let safeMinimumChunkSize = 128 * 1024  // Reduced minimum for constrained systems
2338 |         let defaultChunkSize = 512 * 1024  // Standard default / minimum for non-constrained
2339 |         let constrainedCap = 512 * 1024  // Lower cap for constrained systems
2340 |         let standardCap = 2 * 1024 * 1024  // Standard cap for non-constrained systems
2341 | 
2342 |         // If we can't get memory info, return a reasonable default
2343 |         guard result == KERN_SUCCESS else {
2344 |             Logger.info(
2345 |                 "Could not get VM statistics, using default chunk size: \(defaultChunkSize) bytes")
2346 |             return defaultChunkSize
2347 |         }
2348 | 
2349 |         // Calculate free memory in bytes
2350 |         let pageSize = 4096  // Use a constant page size assumption
2351 |         let freeMemory = UInt64(stats.free_count) * UInt64(pageSize)
2352 |         let isConstrained = determineIfMemoryConstrained()  // Check if generally constrained
2353 | 
2354 |         // Extremely constrained (< 512MB free) -> use absolute minimum
2355 |         if freeMemory < 536_870_912 {  // 512MB
2356 |             Logger.info(
2357 |                 "System extremely memory constrained (<512MB free), using minimum chunk size: \(safeMinimumChunkSize) bytes"
2358 |             )
2359 |             return safeMinimumChunkSize
2360 |         }
2361 | 
2362 |         // Generally constrained -> use adaptive size with lower cap
2363 |         if isConstrained {
2364 |             let adaptiveSize = min(
2365 |                 max(Int(freeMemory / 1000), safeMinimumChunkSize), constrainedCap)
2366 |             Logger.info(
2367 |                 "System memory constrained, using adaptive chunk size capped at \(constrainedCap) bytes: \(adaptiveSize) bytes"
2368 |             )
2369 |             return adaptiveSize
2370 |         }
2371 | 
2372 |         // Not constrained -> use original adaptive logic with standard cap
2373 |         let adaptiveSize = min(max(Int(freeMemory / 1000), defaultChunkSize), standardCap)
2374 |         Logger.info(
2375 |             "System has sufficient memory, using adaptive chunk size capped at \(standardCap) bytes: \(adaptiveSize) bytes"
2376 |         )
2377 |         return adaptiveSize
2378 |     }
2379 | 
2380 |     // Check if system is memory constrained for more aggressive memory management
2381 |     private func determineIfMemoryConstrained() -> Bool {
2382 |         var stats = vm_statistics64_data_t()
2383 |         var size = mach_msg_type_number_t(
2384 |             MemoryLayout<vm_statistics64_data_t>.size / MemoryLayout<integer_t>.size)
2385 |         let hostPort = mach_host_self()
2386 | 
2387 |         let result = withUnsafeMutablePointer(to: &stats) { statsPtr in
2388 |             statsPtr.withMemoryRebound(to: integer_t.self, capacity: Int(size)) { ptr in
2389 |                 host_statistics64(hostPort, HOST_VM_INFO64, ptr, &size)
2390 |             }
2391 |         }
2392 | 
2393 |         guard result == KERN_SUCCESS else {
2394 |             // If we can't determine, assume constrained for safety
2395 |             return true
2396 |         }
2397 | 
2398 |         // Calculate free memory in bytes using a fixed page size
2399 |         // Standard page size on macOS is 4KB or 16KB
2400 |         let pageSize = 4096  // Use a constant instead of vm_kernel_page_size
2401 |         let freeMemory = UInt64(stats.free_count) * UInt64(pageSize)
2402 | 
2403 |         // Consider memory constrained if less than 2GB free
2404 |         return freeMemory < 2_147_483_648  // 2GB
2405 |     }
2406 | 
2407 |     // Helper method to determine network quality
2408 |     private func determineNetworkQuality() -> Int {
2409 |         // Default quality is medium (3)
2410 |         var quality = 3
2411 | 
2412 |         // A simple ping test to determine network quality
2413 |         let process = Process()
2414 |         process.executableURL = URL(fileURLWithPath: "/sbin/ping")
2415 |         process.arguments = ["-c", "3", "-q", self.registry]
2416 | 
2417 |         let outputPipe = Pipe()
2418 |         process.standardOutput = outputPipe
2419 |         process.standardError = outputPipe
2420 | 
2421 |         do {
2422 |             try process.run()
2423 |             process.waitUntilExit()
2424 | 
2425 |             let outputData = try outputPipe.fileHandleForReading.readToEnd() ?? Data()
2426 |             if let output = String(data: outputData, encoding: .utf8) {
2427 |                 // Check for average ping time
2428 |                 if let avgTimeRange = output.range(
2429 |                     of: "= [0-9.]+/([0-9.]+)/", options: .regularExpression)
2430 |                 {
2431 |                     let avgSubstring = output[avgTimeRange]
2432 |                     if let avgString = avgSubstring.split(separator: "/").dropFirst().first,
2433 |                         let avgTime = Double(avgString)
2434 |                     {
2435 | 
2436 |                         // Classify network quality based on ping time
2437 |                         if avgTime < 50 {
2438 |                             quality = 5  // Excellent
2439 |                         } else if avgTime < 100 {
2440 |                             quality = 4  // Good
2441 |                         } else if avgTime < 200 {
2442 |                             quality = 3  // Average
2443 |                         } else if avgTime < 300 {
2444 |                             quality = 2  // Poor
2445 |                         } else {
2446 |                             quality = 1  // Very poor
2447 |                         }
2448 |                     }
2449 |                 }
2450 |             }
2451 |         } catch {
2452 |             // Default to medium if ping fails
2453 |             Logger.info("Failed to determine network quality, using default settings")
2454 |         }
2455 | 
2456 |         return quality
2457 |     }
2458 | 
2459 |     // Helper method to calculate optimal concurrency based on system capabilities
2460 |     private func calculateOptimalConcurrency(memoryConstrained: Bool, networkQuality: Int) -> Int {
2461 |         // Base concurrency based on network quality (1-5)
2462 |         let baseThreads = min(networkQuality * 2, 8)
2463 | 
2464 |         if memoryConstrained {
2465 |             // Reduce concurrency for memory-constrained systems
2466 |             return max(2, baseThreads / 2)
2467 |         }
2468 | 
2469 |         // Physical cores available on the system
2470 |         let cores = ProcessInfo.processInfo.processorCount
2471 | 
2472 |         // Adaptive approach: 1-2 threads per core depending on network quality
2473 |         let threadsPerCore = (networkQuality >= 4) ? 2 : 1
2474 |         let systemBasedThreads = min(cores * threadsPerCore, 12)
2475 | 
2476 |         // Take the larger of network-based and system-based concurrency
2477 |         return max(baseThreads, systemBasedThreads)
2478 |     }
2479 | 
2480 |     // Helper to get optimal TCP window size
2481 |     private func getTCPReceiveWindowSize() -> Int? {
2482 |         // Try to query system TCP window size
2483 |         let process = Process()
2484 |         process.executableURL = URL(fileURLWithPath: "/usr/sbin/sysctl")
2485 |         process.arguments = ["net.inet.tcp.recvspace"]
2486 | 
2487 |         let outputPipe = Pipe()
2488 |         process.standardOutput = outputPipe
2489 | 
2490 |         do {
2491 |             try process.run()
2492 |             process.waitUntilExit()
2493 | 
2494 |             let outputData = try outputPipe.fileHandleForReading.readToEnd() ?? Data()
2495 |             if let output = String(data: outputData, encoding: .utf8),
2496 |                 let valueStr = output.split(separator: ":").last?.trimmingCharacters(
2497 |                     in: .whitespacesAndNewlines),
2498 |                 let value = Int(valueStr)
2499 |             {
2500 |                 return value
2501 |             }
2502 |         } catch {
2503 |             // Ignore errors, we'll use defaults
2504 |         }
2505 | 
2506 |         return nil
2507 |     }
2508 | 
2509 |     // Add helper to check media type and get decompress command
2510 |     private func getDecompressionCommand(for mediaType: String) -> String? {
2511 |         // Determine appropriate decompression command based on layer media type
2512 |         Logger.info("Determining decompression command for media type: \(mediaType)")
2513 | 
2514 |         // For the specific format that appears in our GHCR repository, skip decompression attempts
2515 |         // These files are labeled +lzfse but aren't actually in Apple Archive format
2516 |         if mediaType.contains("+lzfse;part.number=") {
2517 |             Logger.info("Detected LZFSE part file, using direct copy instead of decompression")
2518 |             return nil
2519 |         }
2520 | 
2521 |         // Check for LZFSE or Apple Archive format anywhere in the media type string
2522 |         // The format may include part information like: application/octet-stream+lzfse;part.number=1;part.total=38
2523 |         if mediaType.contains("+lzfse") || mediaType.contains("+aa") {
2524 |             // Apple Archive format requires special handling
2525 |             if let aaPath = findExecutablePath(for: "aa") {
2526 |                 Logger.info("Found Apple Archive tool at: \(aaPath)")
2527 |                 return "apple_archive:\(aaPath)"
2528 |             } else {
2529 |                 Logger.error(
2530 |                     "Apple Archive tool (aa) not found in PATH, falling back to default path")
2531 | 
2532 |                 // Check if the default path exists
2533 |                 let defaultPath = "/usr/bin/aa"
2534 |                 if FileManager.default.isExecutableFile(atPath: defaultPath) {
2535 |                     Logger.info("Default Apple Archive tool exists at: \(defaultPath)")
2536 |                 } else {
2537 |                     Logger.error("Default Apple Archive tool not found at: \(defaultPath)")
2538 |                 }
2539 | 
2540 |                 return "apple_archive:/usr/bin/aa"
2541 |             }
2542 |         } else {
2543 |             Logger.info(
2544 |                 "Unsupported media type: \(mediaType) - only Apple Archive (+lzfse/+aa) is supported"
2545 |             )
2546 |             return nil
2547 |         }
2548 |     }
2549 | 
2550 |     // Helper to find executables (optional, or hardcode paths)
2551 |     private func findExecutablePath(for executableName: String) -> String? {
2552 |         let pathEnv =
2553 |             ProcessInfo.processInfo.environment["PATH"]
2554 |             ?? "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin"
2555 |         let paths = pathEnv.split(separator: ":")
2556 |         for path in paths {
2557 |             let executablePath = URL(fileURLWithPath: String(path)).appendingPathComponent(
2558 |                 executableName
2559 |             ).path
2560 |             if FileManager.default.isExecutableFile(atPath: executablePath) {
2561 |                 return executablePath
2562 |             }
2563 |         }
2564 |         return nil
2565 |     }
2566 | 
2567 |     // Helper function to extract uncompressed disk size from config.json
2568 |     private func getUncompressedSizeFromConfig(configPath: URL) -> UInt64? {
2569 |         guard FileManager.default.fileExists(atPath: configPath.path) else {
2570 |             Logger.info("Config file not found: \(configPath.path)")
2571 |             return nil
2572 |         }
2573 | 
2574 |         do {
2575 |             let configData = try Data(contentsOf: configPath)
2576 |             let decoder = JSONDecoder()
2577 |             let ociConfig = try decoder.decode(OCIConfig.self, from: configData)
2578 | 
2579 |             if let sizeString = ociConfig.annotations?.uncompressedSize,
2580 |                 let size = UInt64(sizeString)
2581 |             {
2582 |                 Logger.info("Found uncompressed disk size annotation: \(size) bytes")
2583 |                 return size
2584 |             } else {
2585 |                 Logger.info("No uncompressed disk size annotation found in config.json")
2586 |                 return nil
2587 |             }
2588 |         } catch {
2589 |             Logger.error("Failed to parse config.json for uncompressed size: \(error)")
2590 |             return nil
2591 |         }
2592 |     }
2593 | 
2594 |     // Helper function to find formatted file with potential extensions
2595 |     private func findFormattedFile(tempFormatted: URL) -> URL? {
2596 |         // Check for the exact path first
2597 |         if FileManager.default.fileExists(atPath: tempFormatted.path) {
2598 |             return tempFormatted
2599 |         }
2600 | 
2601 |         // Check with .dmg extension
2602 |         let dmgPath = tempFormatted.path + ".dmg"
2603 |         if FileManager.default.fileExists(atPath: dmgPath) {
2604 |             return URL(fileURLWithPath: dmgPath)
2605 |         }
2606 | 
2607 |         // Check with .sparseimage extension
2608 |         let sparsePath = tempFormatted.path + ".sparseimage"
2609 |         if FileManager.default.fileExists(atPath: sparsePath) {
2610 |             return URL(fileURLWithPath: sparsePath)
2611 |         }
2612 | 
2613 |         // Try to find any file with the same basename
2614 |         do {
2615 |             let files = try FileManager.default.contentsOfDirectory(
2616 |                 at: tempFormatted.deletingLastPathComponent(),
2617 |                 includingPropertiesForKeys: nil)
2618 |             if let matchingFile = files.first(where: {
2619 |                 $0.lastPathComponent.starts(with: tempFormatted.lastPathComponent)
2620 |             }) {
2621 |                 return matchingFile
2622 |             }
2623 |         } catch {
2624 |             Logger.error("Failed to list directory contents: \(error)")
2625 |         }
2626 | 
2627 |         return nil
2628 |     }
2629 | 
2630 |     // Helper function to decompress LZFSE compressed disk image
2631 |     @discardableResult
2632 |     private func decompressLZFSEImage(inputPath: String, outputPath: String? = nil) -> Bool {
2633 |         Logger.info("Attempting to decompress LZFSE compressed disk image using sparse pipe...")
2634 | 
2635 |         let finalOutputPath = outputPath ?? inputPath  // If outputPath is nil, we'll overwrite input
2636 |         let tempFinalPath = finalOutputPath + ".ddsparse.tmp"  // Temporary name during dd operation
2637 | 
2638 |         // Ensure the temporary file doesn't exist from a previous failed run
2639 |         try? FileManager.default.removeItem(atPath: tempFinalPath)
2640 | 
2641 |         // Process 1: compression_tool
2642 |         let process1 = Process()
2643 |         process1.executableURL = URL(fileURLWithPath: "/usr/bin/compression_tool")
2644 |         process1.arguments = [
2645 |             "-decode",
2646 |             "-i", inputPath,
2647 |             "-o", "/dev/stdout",  // Write to standard output
2648 |         ]
2649 | 
2650 |         // Process 2: dd
2651 |         let process2 = Process()
2652 |         process2.executableURL = URL(fileURLWithPath: "/bin/dd")
2653 |         process2.arguments = [
2654 |             "if=/dev/stdin",  // Read from standard input
2655 |             "of=\(tempFinalPath)",  // Write to the temporary final path
2656 |             "conv=sparse",  // Use sparse conversion
2657 |             "bs=1m",  // Use a reasonable block size (e.g., 1MB)
2658 |         ]
2659 | 
2660 |         // Create pipes
2661 |         let pipe = Pipe()  // Connects process1 stdout to process2 stdin
2662 |         let errorPipe1 = Pipe()
2663 |         let errorPipe2 = Pipe()
2664 | 
2665 |         process1.standardOutput = pipe
2666 |         process1.standardError = errorPipe1
2667 | 
2668 |         process2.standardInput = pipe
2669 |         process2.standardError = errorPipe2
2670 | 
2671 |         do {
2672 |             Logger.info("Starting decompression pipe: compression_tool | dd conv=sparse...")
2673 |             // Start processes
2674 |             try process1.run()
2675 |             try process2.run()
2676 | 
2677 |             // Close the write end of the pipe for process2 to prevent hanging
2678 |             // This might not be strictly necessary if process1 exits cleanly, but safer.
2679 |             // Note: Accessing fileHandleForWriting after run can be tricky.
2680 |             // We rely on process1 exiting to signal EOF to process2.
2681 | 
2682 |             process1.waitUntilExit()
2683 |             process2.waitUntilExit()  // Wait for dd to finish processing the stream
2684 | 
2685 |             // --- Check for errors ---
2686 |             let errorData1 = errorPipe1.fileHandleForReading.readDataToEndOfFile()
2687 |             if !errorData1.isEmpty,
2688 |                 let errorString = String(data: errorData1, encoding: .utf8)?.trimmingCharacters(
2689 |                     in: .whitespacesAndNewlines), !errorString.isEmpty
2690 |             {
2691 |                 Logger.error("compression_tool stderr: \(errorString)")
2692 |             }
2693 |             let errorData2 = errorPipe2.fileHandleForReading.readDataToEndOfFile()
2694 |             if !errorData2.isEmpty,
2695 |                 let errorString = String(data: errorData2, encoding: .utf8)?.trimmingCharacters(
2696 |                     in: .whitespacesAndNewlines), !errorString.isEmpty
2697 |             {
2698 |                 // dd often reports blocks in/out to stderr, filter that if needed, but log for now
2699 |                 Logger.info("dd stderr: \(errorString)")
2700 |             }
2701 | 
2702 |             // Check termination statuses
2703 |             let status1 = process1.terminationStatus
2704 |             let status2 = process2.terminationStatus
2705 | 
2706 |             if status1 != 0 || status2 != 0 {
2707 |                 Logger.error(
2708 |                     "Pipe command failed. compression_tool status: \(status1), dd status: \(status2)"
2709 |                 )
2710 |                 try? FileManager.default.removeItem(atPath: tempFinalPath)  // Clean up failed attempt
2711 |                 return false
2712 |             }
2713 | 
2714 |             // --- Validation ---
2715 |             if FileManager.default.fileExists(atPath: tempFinalPath) {
2716 |                 let fileSize =
2717 |                     (try? FileManager.default.attributesOfItem(atPath: tempFinalPath)[.size]
2718 |                         as? UInt64) ?? 0
2719 |                 let actualUsage = getActualDiskUsage(path: tempFinalPath)
2720 |                 Logger.info(
2721 |                     "Piped decompression successful - Allocated: \(ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: .file)), Actual Usage: \(ByteCountFormatter.string(fromByteCount: Int64(actualUsage), countStyle: .file))"
2722 |                 )
2723 | 
2724 |                 // Basic header validation
2725 |                 var isValid = false
2726 |                 if let fileHandle = FileHandle(forReadingAtPath: tempFinalPath) {
2727 |                     if let data = try? fileHandle.read(upToCount: 512), data.count >= 512,
2728 |                         data[510] == 0x55 && data[511] == 0xAA
2729 |                     {
2730 |                         isValid = true
2731 |                     }
2732 |                     // Ensure handle is closed regardless of validation outcome
2733 |                     try? fileHandle.close()
2734 |                 } else {
2735 |                     Logger.error(
2736 |                         "Validation Error: Could not open decompressed file handle for reading.")
2737 |                 }
2738 | 
2739 |                 if isValid {
2740 |                     Logger.info("Decompressed file appears to be a valid disk image.")
2741 | 
2742 |                     // Move the final file into place
2743 |                     // If outputPath was nil, we need to replace the original inputPath
2744 |                     if outputPath == nil {
2745 |                         // Backup original only if it's different from the temp path
2746 |                         if inputPath != tempFinalPath {
2747 |                             try? FileManager.default.copyItem(
2748 |                                 at: URL(fileURLWithPath: inputPath),
2749 |                                 to: URL(fileURLWithPath: inputPath + ".compressed.bak"))
2750 |                             try? FileManager.default.removeItem(at: URL(fileURLWithPath: inputPath))
2751 |                         }
2752 |                         try FileManager.default.moveItem(
2753 |                             at: URL(fileURLWithPath: tempFinalPath),
2754 |                             to: URL(fileURLWithPath: inputPath))
2755 |                         Logger.info("Replaced original file with sparsely decompressed version.")
2756 |                     } else {
2757 |                         // If outputPath was specified, move it there (overwrite if needed)
2758 |                         try? FileManager.default.removeItem(
2759 |                             at: URL(fileURLWithPath: finalOutputPath))  // Remove existing if overwriting
2760 |                         try FileManager.default.moveItem(
2761 |                             at: URL(fileURLWithPath: tempFinalPath),
2762 |                             to: URL(fileURLWithPath: finalOutputPath))
2763 |                         Logger.info("Moved sparsely decompressed file to: \(finalOutputPath)")
2764 |                     }
2765 |                     return true
2766 |                 } else {
2767 |                     Logger.error(
2768 |                         "Validation failed: Decompressed file header is invalid or file couldn't be read. Cleaning up."
2769 |                     )
2770 |                     try? FileManager.default.removeItem(atPath: tempFinalPath)
2771 |                     return false
2772 |                 }
2773 |             } else {
2774 |                 Logger.error(
2775 |                     "Piped decompression failed: Output file '\(tempFinalPath)' not found after dd completed."
2776 |                 )
2777 |                 return false
2778 |             }
2779 | 
2780 |         } catch {
2781 |             Logger.error("Error running decompression pipe command: \(error)")
2782 |             try? FileManager.default.removeItem(atPath: tempFinalPath)  // Clean up on error
2783 |             return false
2784 |         }
2785 |     }
2786 | 
2787 |     // Helper function to get actual disk usage of a file
2788 |     private func getActualDiskUsage(path: String) -> UInt64 {
2789 |         let task = Process()
2790 |         task.executableURL = URL(fileURLWithPath: "/usr/bin/du")
2791 |         task.arguments = ["-k", path]  // -k for 1024-byte blocks
2792 | 
2793 |         let pipe = Pipe()
2794 |         task.standardOutput = pipe
2795 | 
2796 |         do {
2797 |             try task.run()
2798 |             task.waitUntilExit()
2799 | 
2800 |             let data = pipe.fileHandleForReading.readDataToEndOfFile()
2801 |             if let output = String(data: data, encoding: .utf8),
2802 |                 let size = UInt64(output.split(separator: "\t").first ?? "0")
2803 |             {
2804 |                 return size * 1024  // Convert from KB to bytes
2805 |             }
2806 |         } catch {
2807 |             Logger.error("Failed to get actual disk usage: \(error)")
2808 |         }
2809 | 
2810 |         return 0
2811 |     }
2812 | 
2813 |     // New push method
2814 |     public func push(
2815 |         vmDirPath: String,
2816 |         imageName: String,
2817 |         tags: [String],
2818 |         chunkSizeMb: Int = 512,
2819 |         verbose: Bool = false,
2820 |         dryRun: Bool = false,
2821 |         reassemble: Bool = false
2822 |     ) async throws {
2823 |         Logger.info(
2824 |             "Pushing VM to registry",
2825 |             metadata: [
2826 |                 "vm_path": vmDirPath,
2827 |                 "imageName": imageName,
2828 |                 "tags": "\(tags.joined(separator: ", "))",  // Log all tags
2829 |                 "registry": registry,
2830 |                 "organization": organization,
2831 |                 "chunk_size": "\(chunkSizeMb)MB",
2832 |                 "dry_run": "\(dryRun)",
2833 |                 "reassemble": "\(reassemble)",
2834 |             ])
2835 | 
2836 |         // Check for credentials if not in dry-run mode
2837 |         if !dryRun {
2838 |             let (username, token) = getCredentialsFromEnvironment()
2839 |             if username == nil || token == nil {
2840 |                 Logger.error(
2841 |                     "Missing GitHub credentials. Please set GITHUB_USERNAME and GITHUB_TOKEN environment variables"
2842 |                 )
2843 |                 Logger.error(
2844 |                     "Your token must have 'packages:read' and 'packages:write' permissions")
2845 |                 throw PushError.authenticationFailed
2846 |             }
2847 | 
2848 |             Logger.info("Using GitHub credentials from environment variables")
2849 |         }
2850 | 
2851 |         // Remove tag parsing here, imageName is now passed directly
2852 |         // let components = image.split(separator: ":") ...
2853 |         // let imageTag = String(tag)
2854 | 
2855 |         // Get authentication token only if not in dry-run mode
2856 |         var token: String = ""
2857 |         if !dryRun {
2858 |             Logger.info("Getting registry authentication token")
2859 |             token = try await getToken(
2860 |                 repository: "\(self.organization)/\(imageName)",
2861 |                 scopes: ["pull", "push"],
2862 |                 requireAllScopes: true)  // Require push scope, don't fall back to pull-only
2863 |         } else {
2864 |             Logger.info("Dry run mode: skipping authentication token request")
2865 |         }
2866 | 
2867 |         // Create working directory inside the VM folder for caching/resuming
2868 |         let workDir = URL(fileURLWithPath: vmDirPath).appendingPathComponent(".lume_push_cache")
2869 |         try FileManager.default.createDirectory(at: workDir, withIntermediateDirectories: true)
2870 |         Logger.info("Using push cache directory: \(workDir.path)")
2871 | 
2872 |         // Get VM files that need to be pushed using vmDirPath
2873 |         let diskPath = URL(fileURLWithPath: vmDirPath).appendingPathComponent("disk.img")
2874 |         let configPath = URL(fileURLWithPath: vmDirPath).appendingPathComponent("config.json")
2875 |         let nvramPath = URL(fileURLWithPath: vmDirPath).appendingPathComponent("nvram.bin")
2876 | 
2877 |         var layers: [OCIManifestLayer] = []
2878 |         var uncompressedDiskSize: UInt64? = nil
2879 | 
2880 |         // Process config.json
2881 |         let cachedConfigPath = workDir.appendingPathComponent("config.json")
2882 |         var configDigest: String? = nil
2883 |         var configSize: Int? = nil
2884 | 
2885 |         if FileManager.default.fileExists(atPath: cachedConfigPath.path) {
2886 |             Logger.info("Using cached config.json")
2887 |             do {
2888 |                 let configData = try Data(contentsOf: cachedConfigPath)
2889 |                 configDigest = "sha256:" + configData.sha256String()
2890 |                 configSize = configData.count
2891 |                 // Try to get uncompressed disk size from cached config
2892 |                 if let vmConfig = try? JSONDecoder().decode(VMConfig.self, from: configData) {
2893 |                     uncompressedDiskSize = vmConfig.diskSize
2894 |                     Logger.info(
2895 |                         "Found disk size in cached config: \(uncompressedDiskSize ?? 0) bytes")
2896 |                 }
2897 |             } catch {
2898 |                 Logger.error("Failed to read cached config.json: \(error). Will re-process.")
2899 |                 // Force re-processing by leaving configDigest nil
2900 |             }
2901 |         } else if FileManager.default.fileExists(atPath: configPath.path) {
2902 |             Logger.info("Processing config.json")
2903 |             let configData = try Data(contentsOf: configPath)
2904 |             configDigest = "sha256:" + configData.sha256String()
2905 |             configSize = configData.count
2906 |             try configData.write(to: cachedConfigPath)  // Save to cache
2907 |             // Try to get uncompressed disk size from original config
2908 |             if let vmConfig = try? JSONDecoder().decode(VMConfig.self, from: configData) {
2909 |                 uncompressedDiskSize = vmConfig.diskSize
2910 |                 Logger.info("Found disk size in config: \(uncompressedDiskSize ?? 0) bytes")
2911 |             }
2912 |         }
2913 | 
2914 |         if var digest = configDigest, let size = configSize {  // Use 'var' to modify if uploaded
2915 |             if !dryRun {
2916 |                 // Upload only if not in dry-run mode and blob doesn't exist
2917 |                 if !(try await blobExists(
2918 |                     repository: "\(self.organization)/\(imageName)", digest: digest, token: token))
2919 |                 {
2920 |                     Logger.info("Uploading config.json blob")
2921 |                     let configData = try Data(contentsOf: cachedConfigPath)  // Read from cache for upload
2922 |                     digest = try await uploadBlobFromData(
2923 |                         repository: "\(self.organization)/\(imageName)",
2924 |                         data: configData,
2925 |                         token: token
2926 |                     )
2927 |                 } else {
2928 |                     Logger.info("Config blob already exists on registry")
2929 |                 }
2930 |             }
2931 |             // Add config layer
2932 |             layers.append(
2933 |                 OCIManifestLayer(
2934 |                     mediaType: "application/vnd.oci.image.config.v1+json",
2935 |                     size: size,
2936 |                     digest: digest
2937 |                 ))
2938 |         }
2939 | 
2940 |         // Process nvram.bin
2941 |         let cachedNvramPath = workDir.appendingPathComponent("nvram.bin")
2942 |         var nvramDigest: String? = nil
2943 |         var nvramSize: Int? = nil
2944 | 
2945 |         if FileManager.default.fileExists(atPath: cachedNvramPath.path) {
2946 |             Logger.info("Using cached nvram.bin")
2947 |             do {
2948 |                 let nvramData = try Data(contentsOf: cachedNvramPath)
2949 |                 nvramDigest = "sha256:" + nvramData.sha256String()
2950 |                 nvramSize = nvramData.count
2951 |             } catch {
2952 |                 Logger.error("Failed to read cached nvram.bin: \(error). Will re-process.")
2953 |             }
2954 |         } else if FileManager.default.fileExists(atPath: nvramPath.path) {
2955 |             Logger.info("Processing nvram.bin")
2956 |             let nvramData = try Data(contentsOf: nvramPath)
2957 |             nvramDigest = "sha256:" + nvramData.sha256String()
2958 |             nvramSize = nvramData.count
2959 |             try nvramData.write(to: cachedNvramPath)  // Save to cache
2960 |         }
2961 | 
2962 |         if var digest = nvramDigest, let size = nvramSize {  // Use 'var'
2963 |             if !dryRun {
2964 |                 // Upload only if not in dry-run mode and blob doesn't exist
2965 |                 if !(try await blobExists(
2966 |                     repository: "\(self.organization)/\(imageName)", digest: digest, token: token))
2967 |                 {
2968 |                     Logger.info("Uploading nvram.bin blob")
2969 |                     let nvramData = try Data(contentsOf: cachedNvramPath)  // Read from cache
2970 |                     digest = try await uploadBlobFromData(
2971 |                         repository: "\(self.organization)/\(imageName)",
2972 |                         data: nvramData,
2973 |                         token: token
2974 |                     )
2975 |                 } else {
2976 |                     Logger.info("NVRAM blob already exists on registry")
2977 |                 }
2978 |             }
2979 |             // Add nvram layer
2980 |             layers.append(
2981 |                 OCIManifestLayer(
2982 |                     mediaType: "application/octet-stream",
2983 |                     size: size,
2984 |                     digest: digest
2985 |                 ))
2986 |         }
2987 | 
2988 |         // Process disk.img
2989 |         if FileManager.default.fileExists(atPath: diskPath.path) {
2990 |             let diskAttributes = try FileManager.default.attributesOfItem(atPath: diskPath.path)
2991 |             let diskSize = diskAttributes[.size] as? UInt64 ?? 0
2992 |             let actualDiskSize = uncompressedDiskSize ?? diskSize
2993 |             Logger.info(
2994 |                 "Processing disk.img in chunks",
2995 |                 metadata: [
2996 |                     "disk_path": diskPath.path, "disk_size": "\(diskSize) bytes",
2997 |                     "actual_size": "\(actualDiskSize) bytes", "chunk_size": "\(chunkSizeMb)MB",
2998 |                 ])
2999 |             let chunksDir = workDir.appendingPathComponent("disk.img.parts")
3000 |             try FileManager.default.createDirectory(
3001 |                 at: chunksDir, withIntermediateDirectories: true)
3002 |             let chunkSizeBytes = chunkSizeMb * 1024 * 1024
3003 |             let totalChunks = Int((diskSize + UInt64(chunkSizeBytes) - 1) / UInt64(chunkSizeBytes))
3004 |             Logger.info("Splitting disk into \(totalChunks) chunks")
3005 |             let fileHandle = try FileHandle(forReadingFrom: diskPath)
3006 |             defer { try? fileHandle.close() }
3007 |             var pushedDiskLayers: [(index: Int, layer: OCIManifestLayer)] = []
3008 |             var diskChunks: [(index: Int, path: URL, digest: String)] = []
3009 | 
3010 |             try await withThrowingTaskGroup(of: (Int, OCIManifestLayer, URL, String).self) {
3011 |                 group in
3012 |                 let maxConcurrency = 4
3013 |                 for chunkIndex in 0..<totalChunks {
3014 |                     if chunkIndex >= maxConcurrency {
3015 |                         if let res = try await group.next() {
3016 |                             pushedDiskLayers.append((res.0, res.1))
3017 |                             diskChunks.append((res.0, res.2, res.3))
3018 |                         }
3019 |                     }
3020 |                     group.addTask { [token, verbose, dryRun, organization, imageName] in
3021 |                         let chunkIndex = chunkIndex
3022 |                         let chunkPath = chunksDir.appendingPathComponent("chunk.\(chunkIndex)")
3023 |                         let metadataPath = chunksDir.appendingPathComponent(
3024 |                             "chunk_metadata.\(chunkIndex).json")
3025 |                         var layer: OCIManifestLayer? = nil
3026 |                         var finalCompressedDigest: String? = nil
3027 |                         if FileManager.default.fileExists(atPath: metadataPath.path),
3028 |                             FileManager.default.fileExists(atPath: chunkPath.path)
3029 |                         {
3030 |                             do {
3031 |                                 let metadataData = try Data(contentsOf: metadataPath)
3032 |                                 let metadata = try JSONDecoder().decode(
3033 |                                     ChunkMetadata.self, from: metadataData)
3034 |                                 Logger.info(
3035 |                                     "Resuming chunk \(chunkIndex + 1)/\(totalChunks) from cache")
3036 |                                 finalCompressedDigest = metadata.compressedDigest
3037 |                                 if !dryRun {
3038 |                                     if !(try await self.blobExists(
3039 |                                         repository: "\(organization)/\(imageName)",
3040 |                                         digest: metadata.compressedDigest, token: token))
3041 |                                     {
3042 |                                         Logger.info("Uploading cached chunk \(chunkIndex + 1) blob")
3043 |                                         _ = try await self.uploadBlobFromPath(
3044 |                                             repository: "\(organization)/\(imageName)",
3045 |                                             path: chunkPath, digest: metadata.compressedDigest,
3046 |                                             token: token)
3047 |                                     } else {
3048 |                                         Logger.info(
3049 |                                             "Chunk \(chunkIndex + 1) blob already exists on registry"
3050 |                                         )
3051 |                                     }
3052 |                                 }
3053 |                                 layer = OCIManifestLayer(
3054 |                                     mediaType: "application/octet-stream+lz4",
3055 |                                     size: metadata.compressedSize,
3056 |                                     digest: metadata.compressedDigest,
3057 |                                     uncompressedSize: metadata.uncompressedSize,
3058 |                                     uncompressedContentDigest: metadata.uncompressedDigest)
3059 |                             } catch {
3060 |                                 Logger.info(
3061 |                                     "Failed to load cached metadata/chunk for index \(chunkIndex): \(error). Re-processing."
3062 |                                 )
3063 |                                 finalCompressedDigest = nil
3064 |                                 layer = nil
3065 |                             }
3066 |                         }
3067 |                         if layer == nil {
3068 |                             Logger.info("Processing chunk \(chunkIndex + 1)/\(totalChunks)")
3069 |                             let localFileHandle = try FileHandle(forReadingFrom: diskPath)
3070 |                             defer { try? localFileHandle.close() }
3071 |                             try localFileHandle.seek(toOffset: UInt64(chunkIndex * chunkSizeBytes))
3072 |                             let chunkData =
3073 |                                 try localFileHandle.read(upToCount: chunkSizeBytes) ?? Data()
3074 |                             let uncompressedSize = UInt64(chunkData.count)
3075 |                             let uncompressedDigest = "sha256:" + chunkData.sha256String()
3076 |                             let compressedData =
3077 |                                 try (chunkData as NSData).compressed(using: .lz4) as Data
3078 |                             let compressedSize = compressedData.count
3079 |                             let compressedDigest = "sha256:" + compressedData.sha256String()
3080 |                             try compressedData.write(to: chunkPath)
3081 |                             let metadata = ChunkMetadata(
3082 |                                 uncompressedDigest: uncompressedDigest,
3083 |                                 uncompressedSize: uncompressedSize,
3084 |                                 compressedDigest: compressedDigest, compressedSize: compressedSize)
3085 |                             let metadataData = try JSONEncoder().encode(metadata)
3086 |                             try metadataData.write(to: metadataPath)
3087 |                             finalCompressedDigest = compressedDigest
3088 |                             if !dryRun {
3089 |                                 if !(try await self.blobExists(
3090 |                                     repository: "\(organization)/\(imageName)",
3091 |                                     digest: compressedDigest, token: token))
3092 |                                 {
3093 |                                     Logger.info("Uploading processed chunk \(chunkIndex + 1) blob")
3094 |                                     _ = try await self.uploadBlobFromPath(
3095 |                                         repository: "\(organization)/\(imageName)", path: chunkPath,
3096 |                                         digest: compressedDigest, token: token)
3097 |                                 } else {
3098 |                                     Logger.info(
3099 |                                         "Chunk \(chunkIndex + 1) blob already exists on registry (processed fresh)"
3100 |                                     )
3101 |                                 }
3102 |                             }
3103 |                             layer = OCIManifestLayer(
3104 |                                 mediaType: "application/octet-stream+lz4", size: compressedSize,
3105 |                                 digest: compressedDigest, uncompressedSize: uncompressedSize,
3106 |                                 uncompressedContentDigest: uncompressedDigest)
3107 |                         }
3108 |                         guard let finalLayer = layer, let finalDigest = finalCompressedDigest else {
3109 |                             throw PushError.blobUploadFailed
3110 |                         }
3111 |                         if verbose {
3112 |                             Logger.info("Finished chunk \(chunkIndex + 1)/\(totalChunks)")
3113 |                         }
3114 |                         return (chunkIndex, finalLayer, chunkPath, finalDigest)
3115 |                     }
3116 |                 }
3117 |                 for try await (index, layer, path, digest) in group {
3118 |                     pushedDiskLayers.append((index, layer))
3119 |                     diskChunks.append((index, path, digest))
3120 |                 }
3121 |             }
3122 |             layers.append(
3123 |                 contentsOf: pushedDiskLayers.sorted { $0.index < $1.index }.map { $0.layer })
3124 |             diskChunks.sort { $0.index < $1.index }
3125 |             Logger.info("All disk chunks processed successfully")
3126 | 
3127 |             // --- Calculate Total Upload Size & Initialize Tracker ---
3128 |             if !dryRun {
3129 |                 var totalUploadSizeBytes: Int64 = 0
3130 |                 var totalUploadFiles: Int = 0
3131 |                 // Add config size if it exists
3132 |                 if let size = configSize {
3133 |                     totalUploadSizeBytes += Int64(size)
3134 |                     totalUploadFiles += 1
3135 |                 }
3136 |                 // Add nvram size if it exists
3137 |                 if let size = nvramSize {
3138 |                     totalUploadSizeBytes += Int64(size)
3139 |                     totalUploadFiles += 1
3140 |                 }
3141 |                 // Add sizes of all compressed disk chunks
3142 |                 let allChunkSizes = diskChunks.compactMap {
3143 |                     try? FileManager.default.attributesOfItem(atPath: $0.path.path)[.size] as? Int64
3144 |                         ?? 0
3145 |                 }
3146 |                 totalUploadSizeBytes += allChunkSizes.reduce(0, +)
3147 |                 totalUploadFiles += totalChunks  // Use totalChunks calculated earlier
3148 | 
3149 |                 if totalUploadSizeBytes > 0 {
3150 |                     Logger.info(
3151 |                         "Initializing upload progress: \(totalUploadFiles) files, total size: \(ByteCountFormatter.string(fromByteCount: totalUploadSizeBytes, countStyle: .file))"
3152 |                     )
3153 |                     await uploadProgress.setTotal(totalUploadSizeBytes, files: totalUploadFiles)
3154 |                     // Print initial progress bar
3155 |                     print(
3156 |                         "[░░░░░░░░░░░░░░░░░░░░] 0% (0/\(totalUploadFiles)) | Initializing upload... | ETA: calculating...     "
3157 |                     )
3158 |                     fflush(stdout)
3159 |                 } else {
3160 |                     Logger.info("No files marked for upload.")
3161 |                 }
3162 |             }
3163 |             // --- End Size Calculation & Init ---
3164 | 
3165 |             // Perform reassembly verification if requested in dry-run mode
3166 |             if dryRun && reassemble {
3167 |                 Logger.info("=== REASSEMBLY MODE ===")
3168 |                 Logger.info("Reassembling chunks to verify integrity...")
3169 |                 let reassemblyDir = workDir.appendingPathComponent("reassembly")
3170 |                 try FileManager.default.createDirectory(
3171 |                     at: reassemblyDir, withIntermediateDirectories: true)
3172 |                 let reassembledFile = reassemblyDir.appendingPathComponent("reassembled_disk.img")
3173 | 
3174 |                 // Pre-allocate a sparse file with the correct size
3175 |                 Logger.info(
3176 |                     "Pre-allocating sparse file of \(ByteCountFormatter.string(fromByteCount: Int64(actualDiskSize), countStyle: .file))..."
3177 |                 )
3178 |                 if FileManager.default.fileExists(atPath: reassembledFile.path) {
3179 |                     try FileManager.default.removeItem(at: reassembledFile)
3180 |                 }
3181 |                 guard FileManager.default.createFile(atPath: reassembledFile.path, contents: nil)
3182 |                 else {
3183 |                     throw PushError.fileCreationFailed(reassembledFile.path)
3184 |                 }
3185 | 
3186 |                 let outputHandle = try FileHandle(forWritingTo: reassembledFile)
3187 |                 defer { try? outputHandle.close() }
3188 | 
3189 |                 // Set the file size without writing data (creates a sparse file)
3190 |                 try outputHandle.truncate(atOffset: actualDiskSize)
3191 | 
3192 |                 // Add test patterns at start and end to verify writability
3193 |                 let testPattern = "LUME_TEST_PATTERN".data(using: .utf8)!
3194 |                 try outputHandle.seek(toOffset: 0)
3195 |                 try outputHandle.write(contentsOf: testPattern)
3196 |                 try outputHandle.seek(toOffset: actualDiskSize - UInt64(testPattern.count))
3197 |                 try outputHandle.write(contentsOf: testPattern)
3198 |                 try outputHandle.synchronize()
3199 | 
3200 |                 Logger.info("Test patterns written to sparse file. File is ready for writing.")
3201 | 
3202 |                 // Track reassembly progress
3203 |                 var reassemblyProgressLogger = ProgressLogger(threshold: 0.05)
3204 |                 var currentOffset: UInt64 = 0
3205 | 
3206 |                 // Process each chunk in order
3207 |                 for (index, cachedChunkPath, _) in diskChunks.sorted(by: { $0.index < $1.index }) {
3208 |                     Logger.info(
3209 |                         "Decompressing & writing part \(index + 1)/\(diskChunks.count): \(cachedChunkPath.lastPathComponent) at offset \(currentOffset)..."
3210 |                     )
3211 | 
3212 |                     // Always seek to the correct position
3213 |                     try outputHandle.seek(toOffset: currentOffset)
3214 | 
3215 |                     // Decompress and write the chunk
3216 |                     let decompressedBytesWritten = try decompressChunkAndWriteSparse(
3217 |                         inputPath: cachedChunkPath.path,
3218 |                         outputHandle: outputHandle,
3219 |                         startOffset: currentOffset
3220 |                     )
3221 | 
3222 |                     currentOffset += decompressedBytesWritten
3223 |                     reassemblyProgressLogger.logProgress(
3224 |                         current: Double(currentOffset) / Double(actualDiskSize),
3225 |                         context: "Reassembling"
3226 |                     )
3227 | 
3228 |                     // Ensure data is written before processing next part
3229 |                     try outputHandle.synchronize()
3230 |                 }
3231 | 
3232 |                 // Finalize progress
3233 |                 reassemblyProgressLogger.logProgress(current: 1.0, context: "Reassembly Complete")
3234 |                 Logger.info("")  // Newline
3235 | 
3236 |                 // Close handle before post-processing
3237 |                 try outputHandle.close()
3238 | 
3239 |                 // Optimize sparseness if on macOS
3240 |                 let optimizedFile = reassemblyDir.appendingPathComponent("optimized_disk.img")
3241 |                 if FileManager.default.fileExists(atPath: "/bin/cp") {
3242 |                     Logger.info("Optimizing sparse file representation...")
3243 | 
3244 |                     let process = Process()
3245 |                     process.executableURL = URL(fileURLWithPath: "/bin/cp")
3246 |                     process.arguments = ["-c", reassembledFile.path, optimizedFile.path]
3247 | 
3248 |                     do {
3249 |                         try process.run()
3250 |                         process.waitUntilExit()
3251 | 
3252 |                         if process.terminationStatus == 0 {
3253 |                             // Get sizes of original and optimized files
3254 |                             let optimizedSize =
3255 |                                 (try? FileManager.default.attributesOfItem(
3256 |                                     atPath: optimizedFile.path)[.size] as? UInt64) ?? 0
3257 |                             let originalUsage = getActualDiskUsage(path: reassembledFile.path)
3258 |                             let optimizedUsage = getActualDiskUsage(path: optimizedFile.path)
3259 | 
3260 |                             Logger.info(
3261 |                                 "Sparse optimization results: Before: \(ByteCountFormatter.string(fromByteCount: Int64(originalUsage), countStyle: .file)) actual usage, After: \(ByteCountFormatter.string(fromByteCount: Int64(optimizedUsage), countStyle: .file)) actual usage (Apparent size: \(ByteCountFormatter.string(fromByteCount: Int64(optimizedSize), countStyle: .file)))"
3262 |                             )
3263 | 
3264 |                             // Replace original with optimized version
3265 |                             try FileManager.default.removeItem(at: reassembledFile)
3266 |                             try FileManager.default.moveItem(
3267 |                                 at: optimizedFile, to: reassembledFile)
3268 |                             Logger.info("Using sparse-optimized file for verification")
3269 |                         } else {
3270 |                             Logger.info(
3271 |                                 "Sparse optimization failed, using original file for verification")
3272 |                             try? FileManager.default.removeItem(at: optimizedFile)
3273 |                         }
3274 |                     } catch {
3275 |                         Logger.info(
3276 |                             "Error during sparse optimization: \(error.localizedDescription)")
3277 |                         try? FileManager.default.removeItem(at: optimizedFile)
3278 |                     }
3279 |                 }
3280 | 
3281 |                 // Verification step
3282 |                 Logger.info("Verifying reassembled file...")
3283 |                 let originalSize = diskSize
3284 |                 let originalDigest = calculateSHA256(filePath: diskPath.path)
3285 |                 let reassembledAttributes = try FileManager.default.attributesOfItem(
3286 |                     atPath: reassembledFile.path)
3287 |                 let reassembledSize = reassembledAttributes[.size] as? UInt64 ?? 0
3288 |                 let reassembledDigest = calculateSHA256(filePath: reassembledFile.path)
3289 | 
3290 |                 // Check actual disk usage
3291 |                 let originalActualSize = getActualDiskUsage(path: diskPath.path)
3292 |                 let reassembledActualSize = getActualDiskUsage(path: reassembledFile.path)
3293 | 
3294 |                 // Report results
3295 |                 Logger.info("Results:")
3296 |                 Logger.info(
3297 |                     "  Original size: \(ByteCountFormatter.string(fromByteCount: Int64(originalSize), countStyle: .file)) (\(originalSize) bytes)"
3298 |                 )
3299 |                 Logger.info(
3300 |                     "  Reassembled size: \(ByteCountFormatter.string(fromByteCount: Int64(reassembledSize), countStyle: .file)) (\(reassembledSize) bytes)"
3301 |                 )
3302 |                 Logger.info("  Original digest: \(originalDigest)")
3303 |                 Logger.info("  Reassembled digest: \(reassembledDigest)")
3304 |                 Logger.info(
3305 |                     "  Original: Apparent size: \(ByteCountFormatter.string(fromByteCount: Int64(originalSize), countStyle: .file)), Actual disk usage: \(ByteCountFormatter.string(fromByteCount: Int64(originalActualSize), countStyle: .file))"
3306 |                 )
3307 |                 Logger.info(
3308 |                     "  Reassembled: Apparent size: \(ByteCountFormatter.string(fromByteCount: Int64(reassembledSize), countStyle: .file)), Actual disk usage: \(ByteCountFormatter.string(fromByteCount: Int64(reassembledActualSize), countStyle: .file))"
3309 |                 )
3310 | 
3311 |                 // Determine if verification was successful
3312 |                 if originalDigest == reassembledDigest {
3313 |                     Logger.info("✅ VERIFICATION SUCCESSFUL: Files are identical")
3314 |                 } else {
3315 |                     Logger.info("❌ VERIFICATION FAILED: Files differ")
3316 | 
3317 |                     if originalSize != reassembledSize {
3318 |                         Logger.info(
3319 |                             "  Size mismatch: Original \(originalSize) bytes, Reassembled \(reassembledSize) bytes"
3320 |                         )
3321 |                     }
3322 | 
3323 |                     // Check sparse file characteristics
3324 |                     Logger.info("Attempting to identify differences...")
3325 |                     Logger.info(
3326 |                         "NOTE: This might be a sparse file issue. The content may be identical, but sparse regions"
3327 |                     )
3328 |                     Logger.info(
3329 |                         "      may be handled differently between the original and reassembled files."
3330 |                     )
3331 | 
3332 |                     if originalActualSize > 0 {
3333 |                         let diffPercentage =
3334 |                             ((Double(reassembledActualSize) - Double(originalActualSize))
3335 |                                 / Double(originalActualSize)) * 100.0
3336 |                         Logger.info(
3337 |                             "  Disk usage difference: \(String(format: "%.2f", diffPercentage))%")
3338 | 
3339 |                         if diffPercentage < -40 {
3340 |                             Logger.info(
3341 |                                 "  ⚠️ WARNING: Reassembled disk uses significantly less space (>40% difference)."
3342 |                             )
3343 |                             Logger.info(
3344 |                                 "  This indicates sparse regions weren't properly preserved and may affect VM functionality."
3345 |                             )
3346 |                         } else if diffPercentage < -10 {
3347 |                             Logger.info(
3348 |                                 "  ⚠️ WARNING: Reassembled disk uses less space (10-40% difference)."
3349 |                             )
3350 |                             Logger.info(
3351 |                                 "  Some sparse regions may not be properly preserved but VM might still function correctly."
3352 |                             )
3353 |                         } else if diffPercentage > 10 {
3354 |                             Logger.info(
3355 |                                 "  ⚠️ WARNING: Reassembled disk uses more space (>10% difference).")
3356 |                             Logger.info(
3357 |                                 "  This is unusual and may indicate improper sparse file handling.")
3358 |                         } else {
3359 |                             Logger.info(
3360 |                                 "  ✓ Disk usage difference is minimal (<10%). VM likely to function correctly."
3361 |                             )
3362 |                         }
3363 |                     }
3364 | 
3365 |                     // Offer recovery option
3366 |                     if originalDigest != reassembledDigest {
3367 |                         Logger.info("")
3368 |                         Logger.info("===== ATTEMPTING RECOVERY ACTION =====")
3369 |                         Logger.info(
3370 |                             "Since verification failed, trying direct copy as a fallback method.")
3371 | 
3372 |                         let fallbackFile = reassemblyDir.appendingPathComponent("fallback_disk.img")
3373 |                         Logger.info("Creating fallback disk image at: \(fallbackFile.path)")
3374 | 
3375 |                         // Try rsync first
3376 |                         let rsyncProcess = Process()
3377 |                         rsyncProcess.executableURL = URL(fileURLWithPath: "/usr/bin/rsync")
3378 |                         rsyncProcess.arguments = [
3379 |                             "-aS", "--progress", diskPath.path, fallbackFile.path,
3380 |                         ]
3381 | 
3382 |                         do {
3383 |                             try rsyncProcess.run()
3384 |                             rsyncProcess.waitUntilExit()
3385 | 
3386 |                             if rsyncProcess.terminationStatus == 0 {
3387 |                                 Logger.info(
3388 |                                     "Direct copy completed with rsync. Fallback image available at: \(fallbackFile.path)"
3389 |                                 )
3390 |                             } else {
3391 |                                 // Try cp -c as fallback
3392 |                                 Logger.info("Rsync failed. Attempting with cp -c command...")
3393 |                                 let cpProcess = Process()
3394 |                                 cpProcess.executableURL = URL(fileURLWithPath: "/bin/cp")
3395 |                                 cpProcess.arguments = ["-c", diskPath.path, fallbackFile.path]
3396 | 
3397 |                                 try cpProcess.run()
3398 |                                 cpProcess.waitUntilExit()
3399 | 
3400 |                                 if cpProcess.terminationStatus == 0 {
3401 |                                     Logger.info(
3402 |                                         "Direct copy completed with cp -c. Fallback image available at: \(fallbackFile.path)"
3403 |                                     )
3404 |                                 } else {
3405 |                                     Logger.info("All recovery attempts failed.")
3406 |                                 }
3407 |                             }
3408 |                         } catch {
3409 |                             Logger.info(
3410 |                                 "Error during recovery attempts: \(error.localizedDescription)")
3411 |                             Logger.info("All recovery attempts failed.")
3412 |                         }
3413 |                     }
3414 |                 }
3415 | 
3416 |                 Logger.info("Reassembled file is available at: \(reassembledFile.path)")
3417 |             }
3418 |         }
3419 | 
3420 |         // --- Manifest Creation & Push ---
3421 |         let manifest = createManifest(
3422 |             layers: layers,
3423 |             configLayerIndex: layers.firstIndex(where: {
3424 |                 $0.mediaType == "application/vnd.oci.image.config.v1+json"
3425 |             }),
3426 |             uncompressedDiskSize: uncompressedDiskSize
3427 |         )
3428 | 
3429 |         // Push manifest only if not in dry-run mode
3430 |         if !dryRun {
3431 |             Logger.info("Pushing manifest(s)")  // Updated log
3432 |             // Serialize the manifest dictionary to Data first
3433 |             let manifestData = try JSONSerialization.data(
3434 |                 withJSONObject: manifest, options: [.prettyPrinted, .sortedKeys])
3435 | 
3436 |             // Loop through tags to push the same manifest data
3437 |             for tag in tags {
3438 |                 Logger.info("Pushing manifest for tag: \(tag)")
3439 |                 try await pushManifest(
3440 |                     repository: "\(self.organization)/\(imageName)",
3441 |                     tag: tag,  // Use the current tag from the loop
3442 |                     manifest: manifestData,  // Pass the serialized Data
3443 |                     token: token  // Token should be in scope here now
3444 |                 )
3445 |             }
3446 |         }
3447 | 
3448 |         // Print final upload summary if not dry run
3449 |         if !dryRun {
3450 |             let stats = await uploadProgress.getUploadStats()
3451 |             Logger.info("\n\(stats.formattedSummary())")  // Add newline for separation
3452 |         }
3453 | 
3454 |         // Clean up cache directory only on successful non-dry-run push
3455 |     }
3456 | 
3457 |     private func createManifest(
3458 |         layers: [OCIManifestLayer], configLayerIndex: Int?, uncompressedDiskSize: UInt64?
3459 |     ) -> [String: Any] {
3460 |         var manifest: [String: Any] = [
3461 |             "schemaVersion": 2,
3462 |             "mediaType": "application/vnd.oci.image.manifest.v1+json",
3463 |             "layers": layers.map { layer in
3464 |                 var layerDict: [String: Any] = [
3465 |                     "mediaType": layer.mediaType,
3466 |                     "size": layer.size,
3467 |                     "digest": layer.digest,
3468 |                 ]
3469 | 
3470 |                 if let uncompressedSize = layer.uncompressedSize {
3471 |                     var annotations: [String: String] = [:]
3472 |                     annotations["org.trycua.lume.uncompressed-size"] = "\(uncompressedSize)"  // Updated prefix
3473 | 
3474 |                     if let digest = layer.uncompressedContentDigest {
3475 |                         annotations["org.trycua.lume.uncompressed-content-digest"] = digest  // Updated prefix
3476 |                     }
3477 | 
3478 |                     layerDict["annotations"] = annotations
3479 |                 }
3480 | 
3481 |                 return layerDict
3482 |             },
3483 |         ]
3484 | 
3485 |         // Add config reference if available
3486 |         if let configIndex = configLayerIndex {
3487 |             let configLayer = layers[configIndex]
3488 |             manifest["config"] = [
3489 |                 "mediaType": configLayer.mediaType,
3490 |                 "size": configLayer.size,
3491 |                 "digest": configLayer.digest,
3492 |             ]
3493 |         }
3494 | 
3495 |         // Add annotations
3496 |         var annotations: [String: String] = [:]
3497 |         annotations["org.trycua.lume.upload-time"] = ISO8601DateFormatter().string(from: Date())  // Updated prefix
3498 | 
3499 |         if let diskSize = uncompressedDiskSize {
3500 |             annotations["org.trycua.lume.uncompressed-disk-size"] = "\(diskSize)"  // Updated prefix
3501 |         }
3502 | 
3503 |         manifest["annotations"] = annotations
3504 | 
3505 |         return manifest
3506 |     }
3507 | 
3508 |     private func uploadBlobFromData(repository: String, data: Data, token: String) async throws
3509 |         -> String
3510 |     {
3511 |         // Calculate digest
3512 |         let digest = "sha256:" + data.sha256String()
3513 | 
3514 |         // Check if blob already exists
3515 |         if try await blobExists(repository: repository, digest: digest, token: token) {
3516 |             Logger.info("Blob already exists: \(digest)")
3517 |             return digest
3518 |         }
3519 | 
3520 |         // Initiate upload
3521 |         let uploadURL = try await startBlobUpload(repository: repository, token: token)
3522 | 
3523 |         // Upload blob
3524 |         try await uploadBlob(url: uploadURL, data: data, digest: digest, token: token)
3525 | 
3526 |         // Report progress
3527 |         await uploadProgress.addProgress(Int64(data.count))
3528 | 
3529 |         return digest
3530 |     }
3531 | 
3532 |     private func uploadBlobFromPath(repository: String, path: URL, digest: String, token: String)
3533 |         async throws -> String
3534 |     {
3535 |         // Check if blob already exists
3536 |         if try await blobExists(repository: repository, digest: digest, token: token) {
3537 |             Logger.info("Blob already exists: \(digest)")
3538 |             return digest
3539 |         }
3540 | 
3541 |         // Initiate upload
3542 |         let uploadURL = try await startBlobUpload(repository: repository, token: token)
3543 | 
3544 |         // Load data from file
3545 |         let data = try Data(contentsOf: path)
3546 | 
3547 |         // Upload blob
3548 |         try await uploadBlob(url: uploadURL, data: data, digest: digest, token: token)
3549 | 
3550 |         // Report progress
3551 |         await uploadProgress.addProgress(Int64(data.count))
3552 | 
3553 |         return digest
3554 |     }
3555 | 
3556 |     private func blobExists(repository: String, digest: String, token: String) async throws -> Bool
3557 |     {
3558 |         let url = URL(string: "https://\(registry)/v2/\(repository)/blobs/\(digest)")!
3559 |         var request = URLRequest(url: url)
3560 |         request.httpMethod = "HEAD"
3561 |         request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
3562 | 
3563 |         let (_, response) = try await URLSession.shared.data(for: request)
3564 | 
3565 |         if let httpResponse = response as? HTTPURLResponse {
3566 |             return httpResponse.statusCode == 200
3567 |         }
3568 | 
3569 |         return false
3570 |     }
3571 | 
3572 |     private func startBlobUpload(repository: String, token: String) async throws -> URL {
3573 |         let url = URL(string: "https://\(registry)/v2/\(repository)/blobs/uploads/")!
3574 |         var request = URLRequest(url: url)
3575 |         request.httpMethod = "POST"
3576 |         request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
3577 |         request.setValue("0", forHTTPHeaderField: "Content-Length")  // Explicitly set Content-Length to 0 for POST
3578 | 
3579 |         let (_, response) = try await URLSession.shared.data(for: request)
3580 | 
3581 |         guard let httpResponse = response as? HTTPURLResponse,
3582 |             httpResponse.statusCode == 202,
3583 |             let locationString = httpResponse.value(forHTTPHeaderField: "Location")
3584 |         else {
3585 |             // Log response details on failure
3586 |             let responseBody =
3587 |                 String(
3588 |                     data: (try? await URLSession.shared.data(for: request).0) ?? Data(),
3589 |                     encoding: .utf8) ?? "(No Body)"
3590 |             Logger.error(
3591 |                 "Failed to initiate blob upload. Status: \( (response as? HTTPURLResponse)?.statusCode ?? 0 ). Headers: \( (response as? HTTPURLResponse)?.allHeaderFields ?? [:] ). Body: \(responseBody)"
3592 |             )
3593 |             throw PushError.uploadInitiationFailed
3594 |         }
3595 | 
3596 |         // Construct the base URL for the registry
3597 |         guard let baseRegistryURL = URL(string: "https://\(registry)") else {
3598 |             Logger.error("Failed to create base registry URL from: \(registry)")
3599 |             throw PushError.invalidURL
3600 |         }
3601 | 
3602 |         // Create the final upload URL, resolving the location against the base URL
3603 |         guard let uploadURL = URL(string: locationString, relativeTo: baseRegistryURL) else {
3604 |             Logger.error(
3605 |                 "Failed to create absolute upload URL from location: \(locationString) relative to base: \(baseRegistryURL.absoluteString)"
3606 |             )
3607 |             throw PushError.invalidURL
3608 |         }
3609 | 
3610 |         Logger.info("Blob upload initiated. Upload URL: \(uploadURL.absoluteString)")
3611 |         return uploadURL.absoluteURL  // Ensure it's absolute
3612 |     }
3613 | 
3614 |     private func uploadBlob(url: URL, data: Data, digest: String, token: String) async throws {
3615 |         var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
3616 | 
3617 |         // Add digest parameter
3618 |         var queryItems = components.queryItems ?? []
3619 |         queryItems.append(URLQueryItem(name: "digest", value: digest))
3620 |         components.queryItems = queryItems
3621 | 
3622 |         guard let uploadURL = components.url else {
3623 |             throw PushError.invalidURL
3624 |         }
3625 | 
3626 |         var request = URLRequest(url: uploadURL)
3627 |         request.httpMethod = "PUT"
3628 |         request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
3629 |         request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
3630 |         request.setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
3631 |         request.httpBody = data
3632 | 
3633 |         let (_, response) = try await URLSession.shared.data(for: request)
3634 | 
3635 |         guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 201 else {
3636 |             throw PushError.blobUploadFailed
3637 |         }
3638 |     }
3639 | 
3640 |     private func pushManifest(repository: String, tag: String, manifest: Data, token: String)
3641 |         async throws
3642 |     {
3643 |         let url = URL(string: "https://\(registry)/v2/\(repository)/manifests/\(tag)")!
3644 |         var request = URLRequest(url: url)
3645 |         request.httpMethod = "PUT"
3646 |         request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
3647 |         request.setValue(
3648 |             "application/vnd.oci.image.manifest.v1+json", forHTTPHeaderField: "Content-Type")
3649 |         request.httpBody = manifest
3650 | 
3651 |         let (_, response) = try await URLSession.shared.data(for: request)
3652 | 
3653 |         guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 201 else {
3654 |             throw PushError.manifestPushFailed
3655 |         }
3656 |     }
3657 | 
3658 |     private func getCredentialsFromEnvironment() -> (String?, String?) {
3659 |         let username =
3660 |             ProcessInfo.processInfo.environment["GITHUB_USERNAME"]
3661 |             ?? ProcessInfo.processInfo.environment["GHCR_USERNAME"]
3662 |         let password =
3663 |             ProcessInfo.processInfo.environment["GITHUB_TOKEN"]
3664 |             ?? ProcessInfo.processInfo.environment["GHCR_TOKEN"]
3665 |         return (username, password)
3666 |     }
3667 | 
3668 |     // Add these helper methods for dry-run and reassemble implementation
3669 | 
3670 |     // NEW Helper function using Compression framework and sparse writing
3671 |     private func decompressChunkAndWriteSparse(
3672 |         inputPath: String, outputHandle: FileHandle, startOffset: UInt64
3673 |     ) throws -> UInt64 {
3674 |         guard FileManager.default.fileExists(atPath: inputPath) else {
3675 |             Logger.error("Compressed chunk not found at: \(inputPath)")
3676 |             return 0  // Or throw an error
3677 |         }
3678 | 
3679 |         let sourceData = try Data(
3680 |             contentsOf: URL(fileURLWithPath: inputPath), options: .alwaysMapped)
3681 |         var currentWriteOffset = startOffset
3682 |         var totalDecompressedBytes: UInt64 = 0
3683 |         var sourceReadOffset = 0  // Keep track of how much compressed data we've provided
3684 | 
3685 |         // Use the initializer with the readingFrom closure
3686 |         let filter = try InputFilter(.decompress, using: .lz4) { (length: Int) -> Data? in
3687 |             let bytesAvailable = sourceData.count - sourceReadOffset
3688 |             if bytesAvailable == 0 {
3689 |                 return nil  // No more data
3690 |             }
3691 |             let bytesToRead = min(length, bytesAvailable)
3692 |             let chunk = sourceData.subdata(in: sourceReadOffset..<sourceReadOffset + bytesToRead)
3693 |             sourceReadOffset += bytesToRead
3694 |             return chunk
3695 |         }
3696 | 
3697 |         // Process the decompressed output by reading from the filter
3698 |         while let decompressedData = try filter.readData(ofLength: Self.holeGranularityBytes) {
3699 |             if decompressedData.isEmpty { break }  // End of stream
3700 | 
3701 |             // Check if the chunk is all zeros
3702 |             if decompressedData.count == Self.holeGranularityBytes
3703 |                 && decompressedData == Self.zeroChunk
3704 |             {
3705 |                 // It's a zero chunk, just advance the offset, don't write
3706 |                 currentWriteOffset += UInt64(decompressedData.count)
3707 |             } else {
3708 |                 // Not a zero chunk (or a partial chunk at the end), write it
3709 |                 try outputHandle.seek(toOffset: currentWriteOffset)
3710 |                 try outputHandle.write(contentsOf: decompressedData)
3711 |                 currentWriteOffset += UInt64(decompressedData.count)
3712 |             }
3713 |             totalDecompressedBytes += UInt64(decompressedData.count)
3714 |         }
3715 | 
3716 |         // No explicit finalize needed when initialized with source data
3717 | 
3718 |         return totalDecompressedBytes
3719 |     }
3720 | 
3721 |     // Helper function to calculate SHA256 hash of a file
3722 |     private func calculateSHA256(filePath: String) -> String {
3723 |         guard FileManager.default.fileExists(atPath: filePath) else {
3724 |             return "file-not-found"
3725 |         }
3726 | 
3727 |         let process = Process()
3728 |         process.executableURL = URL(fileURLWithPath: "/usr/bin/shasum")
3729 |         process.arguments = ["-a", "256", filePath]
3730 | 
3731 |         let outputPipe = Pipe()
3732 |         process.standardOutput = outputPipe
3733 | 
3734 |         do {
3735 |             try process.run()
3736 |             process.waitUntilExit()
3737 | 
3738 |             if let data = try outputPipe.fileHandleForReading.readToEnd(),
3739 |                 let output = String(data: data, encoding: .utf8)
3740 |             {
3741 |                 return output.components(separatedBy: " ").first ?? "hash-calculation-failed"
3742 |             }
3743 |         } catch {
3744 |             Logger.error("SHA256 calculation failed: \(error)")
3745 |         }
3746 | 
3747 |         return "hash-calculation-failed"
3748 |     }
3749 | }
3750 | 
3751 | actor UploadProgressTracker {
3752 |     private var totalBytes: Int64 = 0
3753 |     private var uploadedBytes: Int64 = 0  // Renamed
3754 |     private var progressLogger = ProgressLogger(threshold: 0.01)
3755 |     private var totalFiles: Int = 0  // Keep track of total items
3756 |     private var completedFiles: Int = 0  // Keep track of completed items
3757 | 
3758 |     // Upload speed tracking
3759 |     private var startTime: Date = Date()
3760 |     private var lastUpdateTime: Date = Date()
3761 |     private var lastUpdateBytes: Int64 = 0
3762 |     private var speedSamples: [Double] = []
3763 |     private var peakSpeed: Double = 0
3764 |     private var totalElapsedTime: TimeInterval = 0
3765 | 
3766 |     // Smoothing factor for speed calculation
3767 |     private var speedSmoothing: Double = 0.3
3768 |     private var smoothedSpeed: Double = 0
3769 | 
3770 |     func setTotal(_ total: Int64, files: Int) {
3771 |         totalBytes = total
3772 |         totalFiles = files
3773 |         startTime = Date()
3774 |         lastUpdateTime = startTime
3775 |         uploadedBytes = 0  // Reset uploaded bytes
3776 |         completedFiles = 0  // Reset completed files
3777 |         smoothedSpeed = 0
3778 |         speedSamples = []
3779 |         peakSpeed = 0
3780 |         totalElapsedTime = 0
3781 |     }
3782 | 
3783 |     func addProgress(_ bytes: Int64) {
3784 |         uploadedBytes += bytes
3785 |         completedFiles += 1  // Increment completed files count
3786 |         let now = Date()
3787 |         let elapsed = now.timeIntervalSince(lastUpdateTime)
3788 | 
3789 |         // Show first progress update immediately, then throttle updates
3790 |         let shouldUpdate =
3791 |             (uploadedBytes <= bytes) || (elapsed >= 0.5) || (completedFiles == totalFiles)
3792 | 
3793 |         if shouldUpdate && totalBytes > 0 {  // Ensure totalBytes is set
3794 |             let currentSpeed = Double(uploadedBytes - lastUpdateBytes) / max(elapsed, 0.001)
3795 |             speedSamples.append(currentSpeed)
3796 | 
3797 |             // Cap samples array
3798 |             if speedSamples.count > 20 {
3799 |                 speedSamples.removeFirst(speedSamples.count - 20)
3800 |             }
3801 | 
3802 |             peakSpeed = max(peakSpeed, currentSpeed)
3803 | 
3804 |             // Apply exponential smoothing
3805 |             if smoothedSpeed == 0 {
3806 |                 smoothedSpeed = currentSpeed
3807 |             } else {
3808 |                 smoothedSpeed = speedSmoothing * currentSpeed + (1 - speedSmoothing) * smoothedSpeed
3809 |             }
3810 | 
3811 |             let recentAvgSpeed = calculateAverageSpeed()
3812 |             let totalElapsed = now.timeIntervalSince(startTime)
3813 |             let overallAvgSpeed = totalElapsed > 0 ? Double(uploadedBytes) / totalElapsed : 0
3814 | 
3815 |             let progress = totalBytes > 0 ? Double(uploadedBytes) / Double(totalBytes) : 1.0  // Avoid division by zero
3816 |             logSpeedProgress(
3817 |                 current: progress,
3818 |                 currentSpeed: currentSpeed,
3819 |                 averageSpeed: recentAvgSpeed,
3820 |                 smoothedSpeed: smoothedSpeed,
3821 |                 overallSpeed: overallAvgSpeed,
3822 |                 peakSpeed: peakSpeed,
3823 |                 context: "Uploading Image"  // Changed context
3824 |             )
3825 | 
3826 |             lastUpdateTime = now
3827 |             lastUpdateBytes = uploadedBytes
3828 |             totalElapsedTime = totalElapsed
3829 |         }
3830 |     }
3831 | 
3832 |     private func calculateAverageSpeed() -> Double {
3833 |         guard !speedSamples.isEmpty else { return 0 }
3834 |         var totalWeight = 0.0
3835 |         var weightedSum = 0.0
3836 |         let samples = speedSamples.suffix(min(8, speedSamples.count))
3837 |         for (index, speed) in samples.enumerated() {
3838 |             let weight = Double(index + 1)
3839 |             weightedSum += speed * weight
3840 |             totalWeight += weight
3841 |         }
3842 |         return totalWeight > 0 ? weightedSum / totalWeight : 0
3843 |     }
3844 | 
3845 |     // Use the UploadStats struct
3846 |     func getUploadStats() -> UploadStats {
3847 |         let avgSpeed = totalElapsedTime > 0 ? Double(uploadedBytes) / totalElapsedTime : 0
3848 |         return UploadStats(
3849 |             totalBytes: totalBytes,
3850 |             uploadedBytes: uploadedBytes,  // Renamed
3851 |             elapsedTime: totalElapsedTime,
3852 |             averageSpeed: avgSpeed,
3853 |             peakSpeed: peakSpeed
3854 |         )
3855 |     }
3856 | 
3857 |     private func logSpeedProgress(
3858 |         current: Double,
3859 |         currentSpeed: Double,
3860 |         averageSpeed: Double,
3861 |         smoothedSpeed: Double,
3862 |         overallSpeed: Double,
3863 |         peakSpeed: Double,
3864 |         context: String
3865 |     ) {
3866 |         let progressPercent = Int(current * 100)
3867 |         // let currentSpeedStr = formatByteSpeed(currentSpeed) // Removed unused
3868 |         let avgSpeedStr = formatByteSpeed(averageSpeed)
3869 |         // let peakSpeedStr = formatByteSpeed(peakSpeed) // Removed unused
3870 |         let remainingBytes = totalBytes - uploadedBytes
3871 |         let speedForEta = max(smoothedSpeed, averageSpeed * 0.8)
3872 |         let etaSeconds = speedForEta > 0 ? Double(remainingBytes) / speedForEta : 0
3873 |         let etaStr = formatTimeRemaining(etaSeconds)
3874 |         let progressBar = createProgressBar(progress: current)
3875 |         let fileProgress = "(\(completedFiles)/\(totalFiles))"  // Add file count
3876 | 
3877 |         print(
3878 |             "\r\(progressBar) \(progressPercent)% \(fileProgress) | Speed: \(avgSpeedStr) (Avg) | ETA: \(etaStr)     ",  // Simplified output
3879 |             terminator: "")
3880 |         fflush(stdout)
3881 |     }
3882 | 
3883 |     // Helper methods (createProgressBar, formatByteSpeed, formatTimeRemaining) remain the same
3884 |     private func createProgressBar(progress: Double, width: Int = 30) -> String {
3885 |         let completedWidth = Int(progress * Double(width))
3886 |         let remainingWidth = width - completedWidth
3887 |         let completed = String(repeating: "█", count: completedWidth)
3888 |         let remaining = String(repeating: "░", count: remainingWidth)
3889 |         return "[\(completed)\(remaining)]"
3890 |     }
3891 |     private func formatByteSpeed(_ bytesPerSecond: Double) -> String {
3892 |         let units = ["B/s", "KB/s", "MB/s", "GB/s"]
3893 |         var speed = bytesPerSecond
3894 |         var unitIndex = 0
3895 |         while speed > 1024 && unitIndex < units.count - 1 {
3896 |             speed /= 1024
3897 |             unitIndex += 1
3898 |         }
3899 |         return String(format: "%.1f %@", speed, units[unitIndex])
3900 |     }
3901 |     private func formatTimeRemaining(_ seconds: Double) -> String {
3902 |         if seconds.isNaN || seconds.isInfinite || seconds <= 0 { return "calculating..." }
3903 |         let hours = Int(seconds) / 3600
3904 |         let minutes = (Int(seconds) % 3600) / 60
3905 |         let secs = Int(seconds) % 60
3906 |         if hours > 0 {
3907 |             return String(format: "%d:%02d:%02d", hours, minutes, secs)
3908 |         } else {
3909 |             return String(format: "%d:%02d", minutes, secs)
3910 |         }
3911 |     }
3912 | }
3913 | 
```
Page 20/20FirstPrevNextLast