|

Initial mercurial commit for Isurus

Author: Chris Tusa <chris.tusa@leafscale.com>
Date: Jun 09, 2026 17:28
Changeset: d0a96d73e9e1ec091d9837ce8b30f767464e7c06
Branch: default
Tags: v0.4.2

Changed files:

Diff

diff -r 000000000000 -r d0a96d73e9e1 .hgignore
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,32 @@
+syntax: glob
+
+# Build output
+build/
+*.o
+*.a
+coral
+
+# Generated C code (use --keep-c to debug)
+*.c.reef.c
+
+# Editor files
+*~
+*.swp
+*.swo
+.vscode/
+.idea/
+.claude/
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# Release
+releases/
+
+# Local project files
+resume
+bugs/
+
+# Claude superpowers scratch (plans, etc.)
+docs/superpowers/
diff -r 000000000000 -r d0a96d73e9e1 .isurus-ci.yml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.isurus-ci.yml	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,34 @@
+container-registry:
+  - url: registry.sharkos.one
+    secret: SHARKOS_REGISTRY_CREDS
+
+when:
+  event: [push, pull_request, tag]     # build on pushes, PRs, and tags;
+                                       # package + release are tag-gated below.
+
+steps:
+  - name: build
+    docker: { image: registry.sharkos.one/reef/reef-builder:latest }
+    commands:
+      - reefc doctor
+      - reefc build
+      - ./build/coral --version
+    timeout: 30m
+
+  - name: package
+    docker: { image: registry.sharkos.one/reef/reef-builder:latest }
+    when: { event: [tag] }
+    commands:
+      - ./scripts/make-release.sh "${CI_TAG#v}"
+      - ls -la releases/
+    timeout: 30m
+
+release:
+  tag: "$CI_TAG"
+  title: "Coral $CI_TAG"
+  assets:
+    - releases/*.tar.xz
+    - releases/*.sha256
+  secret: ISURUS_RELEASE_TOKEN
+  when:
+    event: [tag]
diff -r 000000000000 -r d0a96d73e9e1 .remember/.gitignore
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.remember/.gitignore	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,1 @@
+*
diff -r 000000000000 -r d0a96d73e9e1 .remember/logs/memory-2026-06-09.log
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.remember/logs/memory-2026-06-09.log	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,67 @@
+16:14:20 [hook] session-start: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:16:19 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:16:22 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:16:41 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:16:48 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:23:36 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:24:03 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:24:28 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:24:28 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:24:28 [extract] session 53cf3104-6785-4f13-8a98-9048e7c078b4
+16:24:28 [extract] 17 exchanges (2 human)
+16:24:28 [extract] 2 human msgs < 3, skip
+16:24:36 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:24:36 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:24:36 [cooldown] 8s < 120s, skip
+16:24:53 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:24:53 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:24:53 [cooldown] 25s < 120s, skip
+16:45:19 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:45:19 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:45:19 [extract] session 53cf3104-6785-4f13-8a98-9048e7c078b4
+16:45:19 [extract] 23 exchanges (4 human)
+16:45:19 [haiku] calling (branch: main)
+16:45:43 [tokens] tokens: 9+17847cache→2861out ($0.031154)
+16:45:43 [write] appended: ## 21:45 | main
+16:45:43 [write] position → 69
+16:45:43 [ndc] now.md → today-2026-06-09.md
+16:45:43 [ndc] running (PID 2647879)
+16:45:58 [ndc] tokens: 9+17847cache→1609out ($0.020934)
+16:45:58 [ndc] 218→162b (-25%)
+16:47:22 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:47:31 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:47:37 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:51:38 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:54:48 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:54:55 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:55:05 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:55:36 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:56:41 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:56:49 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:56:49 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:56:49 [extract] session 53cf3104-6785-4f13-8a98-9048e7c078b4
+16:56:49 [extract] 18 exchanges (1 human)
+16:56:49 [extract] 1 human msgs < 3, skip
+16:56:54 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:56:54 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:56:54 [cooldown] 5s < 120s, skip
+16:56:58 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:56:58 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:56:58 [cooldown] 9s < 120s, skip
+16:57:05 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:57:05 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:57:05 [cooldown] 16s < 120s, skip
+16:57:08 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:57:08 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:57:08 [cooldown] 19s < 120s, skip
+16:57:51 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:57:52 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+16:57:52 [cooldown] 63s < 120s, skip
+17:25:30 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+17:25:30 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember
+17:25:30 [extract] session 53cf3104-6785-4f13-8a98-9048e7c078b4
+17:25:30 [extract] 33 exchanges (3 human)
+17:25:30 [haiku] calling (branch: main)
+17:25:48 [tokens] tokens: 9+17847cache→1799out ($0.025559)
+17:25:48 [write] appended: ## 22:25 | main
+17:25:48 [write] position → 160
diff -r 000000000000 -r d0a96d73e9e1 .remember/now.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.remember/now.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,3 @@
+
+## 22:25 | main
+Excluded superpowers/ from release tarball (make-release.sh + .gitignore); verified Coral 0.4.2 build against Reef 0.6.4; archived 120-commit hist to docs/git-history.{md,csv}.
\ No newline at end of file
diff -r 000000000000 -r d0a96d73e9e1 .remember/tmp/last-ndc.ts
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.remember/tmp/last-ndc.ts	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,1 @@
+1781041543
diff -r 000000000000 -r d0a96d73e9e1 .remember/tmp/last-save-ts
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.remember/tmp/last-save-ts	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,1 @@
+1781043930
diff -r 000000000000 -r d0a96d73e9e1 .remember/tmp/last-save.json
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.remember/tmp/last-save.json	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,1 @@
+{"session": "53cf3104-6785-4f13-8a98-9048e7c078b4", "line": 160}
\ No newline at end of file
diff -r 000000000000 -r d0a96d73e9e1 .remember/tmp/save-session.pid
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.remember/tmp/save-session.pid	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,1 @@
+2654712
diff -r 000000000000 -r d0a96d73e9e1 .remember/today-2026-06-09.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.remember/today-2026-06-09.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,2 @@
+## 21:45 | main
+Migr Coral CI Woodpecker→Isurus: .isurus-ci.yml (reef-builder+native release); archived 120 commits MD+CSV; Reef 0.5.20→0.6.4 bump (hardcoded)
\ No newline at end of file
diff -r 000000000000 -r d0a96d73e9e1 README.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,119 @@
+# Coral Package Manager
+
+Coral is the package manager for Zygaena, written in [Reef](https://github.com/reef-lang/reef).
+
+## Building
+
+### Prerequisites
+
+- Reef compiler (`reefc`) version 0.1.6+
+- On illumos: source the development environment first
+
+### Quick Start
+
+```bash
+# On illumos - source the environment
+source /opt/zygaena-env.sh
+
+# Build coral
+cd ~/repos/zygaena/coral
+reefc build -l socket -l nsl
+
+# The binary is created at build/coral
+./build/coral --help
+```
+
+### Build Options
+
+```bash
+# Basic build (Linux/macOS)
+reefc build
+
+# illumos/Solaris (requires socket libraries)
+reefc build -l socket -l nsl
+
+# Type-check only (no compilation)
+reefc build --check
+
+# Verbose output
+reefc build -v
+
+# Keep generated C code for debugging
+reefc build --keep-c
+```
+
+### Installation
+
+After building, install to your PATH:
+
+```bash
+# Option 1: Copy to /usr/local/bin (requires root)
+sudo cp build/coral /usr/local/bin/
+
+# Option 2: Copy to user bin
+mkdir -p ~/bin
+cp build/coral ~/bin/
+export PATH="$HOME/bin:$PATH"
+```
+
+## Usage
+
+```bash
+coral --help              # Show help
+coral build <port>        # Build a package from port
+coral install <pkg>       # Install package(s)
+coral remove <pkg>        # Remove package(s)
+coral upgrade [pkg]       # Upgrade package(s)
+coral info <pkg>          # Show package info
+coral list                # List installed packages
+coral files <pkg>         # List files owned by package
+coral owner <file>        # Find which package owns a file
+coral search <term>       # Search for packages
+coral depends <pkg>       # Show dependency tree
+coral sync                # Update ports tree
+coral outdated            # Show outdated packages
+coral db status           # Show database status
+coral clean status        # Show build artifact status
+coral ports new cat/name  # Scaffold a new port
+coral ports cache status  # Show ports index status
+```
+
+## Project Structure
+
+```
+coral/
+├── reef.toml             # Project manifest
+├── src/
+│   ├── main.reef         # Entry point, CLI dispatch
+│   ├── types.reef        # Shared type definitions
+│   ├── commands/         # Command implementations
+│   │   ├── build.reef
+│   │   ├── install.reef
+│   │   ├── remove.reef
+│   │   └── ...
+│   ├── core/             # Core functionality
+│   │   ├── port.reef     # Port file parsing
+│   │   ├── portindex.reef # Ports index (MsgPack cache)
+│   │   ├── package.reef  # Package operations
+│   │   ├── database.reef # Package database
+│   │   ├── resolver.reef # Dependency resolution
+│   │   └── ...
+│   └── util/             # Utilities
+│       ├── http.reef     # HTTP downloads
+│       ├── archive.reef  # Archive handling
+│       └── ...
+├── test/                 # Tests
+├── build/                # Build output (generated)
+└── docs/                 # Documentation
+```
+
+## Configuration
+
+Coral uses TOML for configuration. See `docs/` for details on:
+- Port file format (`package.toml`)
+- Repository configuration
+- Build scripts (`build.reef`)
+
+## License
+
+BSD-2-Clause
diff -r 000000000000 -r d0a96d73e9e1 docs/GUIDE.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/GUIDE.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,1223 @@
+# Coral Package Manager Guide
+
+Coral is the package manager for Zygaena, a ports-based operating system built on illumos. This guide covers all aspects of using Coral, from basic package management to creating repositories and signing packages.
+
+## Table of Contents
+
+1. [Installation and Setup](#installation-and-setup)
+2. [Basic Usage](#basic-usage)
+3. [Creating and Building Packages](#creating-and-building-packages)
+4. [Package Groups](#package-groups)
+5. [Alternative Dependencies](#alternative-dependencies)
+6. [Creating Repositories](#creating-repositories)
+7. [Package Signing](#package-signing)
+8. [Database Management](#database-management)
+9. [Port Development Tools](#port-development-tools)
+10. [Configuration Reference](#configuration-reference)
+
+---
+
+## Installation and Setup
+
+### Prerequisites
+
+Coral requires:
+- Zygaena or illumos-based system
+- Reef compiler 0.5.0 or later
+- Root privileges for package installation
+- GCC or compatible C compiler (for building from source)
+
+### Directory Structure
+
+Coral uses the following directories:
+
+| Path | Purpose |
+|------|---------|
+| `/usr/zports/` | Ports tree (package build definitions) |
+| `/usr/zports/groups/` | Package group definitions |
+| `/etc/coral/coral.conf` | Main configuration file |
+| `/etc/coral/repos.d/` | Repository configurations |
+| `/etc/coral/keys/` | Signing keys |
+| `/var/lib/coral/installed/` | Installed package database (source of truth) |
+| `/var/lib/coral/db/` | Package database indexes (MsgPack) |
+| `/var/lib/coral/packages/` | Binary package cache |
+| `/var/lib/coral/sources/` | Source archive cache |
+| `/var/lib/coral/repos/` | Repository metadata cache |
+| `/var/lib/coral/trusted-keys/` | Trusted public keys |
+| `/var/tmp/coral/work/` | Build work directory |
+| `/var/tmp/coral/pkg/` | Package staging directory |
+
+### Initial Setup
+
+1. Create required directories:
+```bash
+sudo mkdir -p /usr/zports/{core,base,devel,desktop,extra}
+sudo mkdir -p /usr/zports/groups
+sudo mkdir -p /etc/coral/repos.d
+sudo mkdir -p /etc/coral/keys
+sudo mkdir -p /var/lib/coral/{installed,packages,sources,repos,trusted-keys,db}
+sudo mkdir -p /var/tmp/coral/{work,pkg}
+```
+
+2. Create initial configuration:
+```bash
+sudo tee /etc/coral/coral.conf << 'EOF'
+[general]
+arch = "x86_64"
+jobs = 0
+confirm = true
+color = true
+
+[build]
+logging = true
+quiet = false
+
+[security]
+require_signed_repos = false
+require_signed_packages = false
+EOF
+```
+
+3. Initialize the package database:
+```bash
+sudo coral db init
+```
+
+---
+
+## Basic Usage
+
+### Installing Packages
+
+Install a single package:
+```bash
+coral install vim
+```
+
+Install multiple packages:
+```bash
+coral install vim nano htop
+```
+
+Install from a local package file:
+```bash
+coral install /path/to/package-1.0.0-1.pkg.tar.xz
+```
+
+Install without confirmation:
+```bash
+coral install -y vim
+```
+
+Force reinstall (also overrides conflict checks):
+```bash
+coral install --force vim
+```
+
+Dry run (show what would be done without making changes):
+```bash
+coral install -n vim
+```
+
+When a binary package is not found in any repository and `fallback_to_source = true` (default), Coral will attempt to build the package from the ports tree automatically.
+
+### Removing Packages
+
+Remove a package:
+```bash
+coral remove vim
+```
+
+Remove multiple packages:
+```bash
+coral remove vim nano htop
+```
+
+### Upgrading Packages
+
+Upgrade all packages:
+```bash
+coral upgrade
+```
+
+Upgrade specific packages:
+```bash
+coral upgrade vim nano
+```
+
+Check for available updates:
+```bash
+coral outdated
+```
+
+### Querying Packages
+
+List installed packages:
+```bash
+coral list
+```
+
+Show package information:
+```bash
+coral info vim
+```
+
+List files owned by a package:
+```bash
+coral files vim
+```
+
+Find which package owns a file:
+```bash
+coral owner /usr/bin/vim
+```
+
+Search for packages:
+```bash
+coral search editor
+```
+
+Show dependency tree:
+```bash
+coral depends vim
+```
+
+Verify package integrity:
+```bash
+coral verify vim
+```
+
+### Building Packages
+
+Build a package from source:
+```bash
+coral build vim
+```
+
+The built package is stored in `/var/lib/coral/packages/`.
+
+### Syncing Ports
+
+Update the ports tree:
+```bash
+coral sync
+```
+
+---
+
+## Creating and Building Packages
+
+### Port Structure
+
+A port consists of a directory under `/usr/zports/<category>/<name>/` containing:
+
+```
+/usr/zports/base/mypackage/
+├── package.toml                    # Package metadata
+├── build.sh                        # Build script (zsh)
+├── fix-build.patch                 # Optional patch files
+└── hammerhead-solaris-compat.patch  # (referenced in package.toml [patches])
+```
+
+### package.toml
+
+The `package.toml` file defines package metadata:
+
+```toml
+[package]
+name = "mypackage"
+version = "1.2.3"
+release = 1
+description = "A useful package"
+url = "https://example.com/mypackage"
+license = "MIT"
+maintainer = "Your Name <you@example.com>"
+arch = "x86_64"
+
+# Optional: alternatives this package provides
+alternatives = ["editor", "pager"]
+
+# Optional: packages this conflicts with (install aborts if any are installed)
+conflicts = ["otherpackage"]
+
+[dependencies]
+# Packages required at runtime
+runtime = ["libfoo", "libbar>=1.2"]
+
+# Packages required only for building
+build = ["gcc", "gmake"]
+
+### Per-Package Path Overrides
+
+Packages can override the default installation prefix and sysconfdir. This is useful for packages that install to `/opt`, `/srv`, or other non-standard locations:
+
+```toml
+[paths]
+prefix = "/opt/myapp"
+sysconfdir = "/opt/myapp/etc"
+```
+
+These override the system defaults from `coral.conf` and are exported as `$PREFIX` and `$SYSCONFDIR` to the build script. If omitted, the system-wide values are used.
+
+### Source Files
+
+Source files can be specified using TOML table arrays (`[[source]]`), which supports
+mirror URLs for automatic failover:
+
+```toml
+[[source]]
+file = "mypackage-1.2.3.tar.gz"
+urls = [
+    "https://example.com/mypackage-1.2.3.tar.gz",
+    "https://mirror.example.org/mypackage-1.2.3.tar.gz"
+]
+checksum = "abc123def456..."
+
+[[source]]
+file = "extra-data.tar.gz"
+urls = ["https://example.com/extra-data.tar.gz"]
+checksum = "789xyz..."
+extract = false  # Don't extract this archive
+```
+
+Each `[[source]]` entry supports:
+
+| Field | Required | Description |
+|-------|----------|-------------|
+| `file` | Yes | Output filename to save as |
+| `urls` | Yes | Array of mirror URLs (tried in order until success) |
+| `checksum` | Yes | SHA256 checksum, or `"SKIP"` to skip verification |
+| `extract` | No | Whether to extract the archive (default: `true`) |
+
+**Mirror Failover**: When downloading, Coral tries each URL in the `urls` array
+in order. If a download fails or checksum verification fails, the next mirror
+is tried automatically.
+
+#### Local Source Files
+
+For packages where sources aren't available on public HTTP servers (private repos,
+bundled tarballs), you can place source files directly in the port directory and
+reference them by filename:
+
+```toml
+[[source]]
+file = "reef-lang-0.1.10.tar.gz"
+urls = ["reef-lang-0.1.10.tar.gz"]  # Local file in port directory
+checksum = "abc123..."
+```
+
+URLs that don't start with `http://` or `https://` are treated as local paths:
+- **Relative paths** (no leading `/`) are resolved relative to the port directory
+- **Absolute paths** are used as-is
+
+Local files are copied to the sources cache (`/var/lib/coral/sources/`) for
+consistency with downloaded files.
+
+You can also mix local and remote sources for fallback:
+```toml
+[[source]]
+file = "mypackage-1.0.tar.gz"
+urls = [
+    "mypackage-1.0.tar.gz",                          # Try local first
+    "https://example.com/mypackage-1.0.tar.gz"       # Fall back to remote
+]
+checksum = "abc123..."
+```
+
+#### Legacy Format (Deprecated)
+
+The older `[sources]` format is still supported for backward compatibility:
+
+```toml
+[sources]
+urls = [
+    "https://example.com/mypackage-1.2.3.tar.gz"
+]
+checksums = [
+    "abc123def456..."
+]
+```
+
+**Note**: The legacy format does not support mirrors or the `extract` flag.
+
+### Patches
+
+Coral can apply patches to source code before running `build.sh`. Patch files
+are placed in the port directory alongside `package.toml` and declared in a
+`[patches]` section:
+
+```toml
+[patches]
+files = ["hammerhead-solaris-compat.patch", "fix-build.patch"]
+strip = 0
+```
+
+| Field | Required | Default | Description |
+|-------|----------|---------|-------------|
+| `files` | Yes | — | Array of patch filenames, applied in order |
+| `strip` | No | `0` | Strip level passed to `patch -p<N>` |
+
+Patches are applied from within the extracted source directory using `patch -p<strip>`.
+The `strip` value controls how many leading path components are removed from filenames
+in the diff header:
+- `strip = 0` — paths used as-is (e.g., diff against `configure` / `configure.orig`)
+- `strip = 1` — strips `a/` and `b/` prefixes (standard `git diff` output)
+
+**Example**: Adding Hammerhead OS recognition to ncurses' configure script:
+```diff
+--- configure.orig
++++ configure
+@@ -7066,7 +7066,7 @@
+ 		test "$cf_cv_shlib_version" = auto && cf_cv_shlib_version=rel
+ 		;;
+-	(solaris2*)
++	(solaris2*|hammerhead*)
+ 		# tested with SunOS 5.5.1 (solaris 2.5.1) and gcc 2.7.2
+```
+
+[build]
+# Enable parallel builds
+parallel = true
+
+# Number of parallel jobs (0 = auto)
+jobs = 0
+```
+
+### Build Script (build.sh)
+
+The build script is executed in the source directory. Environment variables available:
+
+| Variable | Description |
+|----------|-------------|
+| `$DESTDIR` | Installation staging directory |
+| `$PREFIX` | Installation prefix (default: `/usr/local`, configurable in `coral.conf` or per-package) |
+| `$SYSCONFDIR` | System configuration directory (default: `/etc`, configurable in `coral.conf` or per-package) |
+| `$PKG_ROOT` | Alternate root directory (empty when installing to `/`) |
+| `$SRCDIR` | Extracted source directory |
+| `$PKGDIR` | Package staging directory (alias for `$DESTDIR`) |
+| `$CFLAGS` | C compiler flags |
+| `$CXXFLAGS` | C++ compiler flags |
+| `$LDFLAGS` | Linker flags (see [Linker Flags](#linker-flags-and-library-resolution) below) |
+| `$PKG_CONFIG_PATH` | pkg-config search path (set to `$PREFIX/lib/pkgconfig`) |
+| `$MAKEFLAGS` | Make flags (includes `-j` for parallel builds) |
+| `$JOBS` | Number of parallel jobs |
+| `$PKG_NAME` | Package name |
+| `$PKG_VERSION` | Package version |
+| `$BUILD_TRIPLE` | Canonical build triplet (e.g., `x86_64-zygaea-hammerhead`) |
+| `$MAKE` | Make command (`gmake`) |
+| `$TAR` | Tar command (`gtar`) |
+
+Example build.sh:
+```bash
+#!/usr/bin/zsh
+set -e
+
+# Configure using $PREFIX and $SYSCONFDIR (don't hardcode paths)
+./configure --prefix=$PREFIX --sysconfdir=$SYSCONFDIR
+
+# Build
+$MAKE ${MAKEFLAGS}
+
+# Install to staging directory
+$MAKE DESTDIR="${DESTDIR}" install
+```
+
+Build scripts are executed with `zsh` (the Zygaena standard shell).
+
+#### Linker Flags and Library Resolution
+
+Coral sets `LDFLAGS="-R$PREFIX/lib"` globally. This embeds an RPATH in every
+built binary so the runtime linker can find shared libraries installed under
+`$PREFIX` (e.g., `/usr/local/lib`) without requiring system-wide `crle`
+configuration.
+
+**Why only `-R` and not `-L`?**
+
+Coral intentionally does **not** set `-L$PREFIX/lib` globally. The `-L` flag
+changes the **build-time** linker search order, which could cause Coral-installed
+libraries to shadow base OS libraries in `/lib` and `/usr/lib`. For example, if
+both the base OS and a Coral port ship `libz.so`, a global `-L/usr/local/lib`
+would cause all ports to link against the Coral version instead of the base
+version, potentially introducing ABI mismatches or version conflicts.
+
+The `-R` flag only affects **runtime** resolution and does not change build-time
+search order, making it safe to set globally.
+
+**When a build script needs `-L`:**
+
+If a port's `configure` script cannot find a dependency's library on its own,
+the build script should add `-L` explicitly:
+
+```bash
+export LDFLAGS="-L$PREFIX/lib $LDFLAGS"
+```
+
+This appends to Coral's RPATH setting rather than replacing it. Only add `-L`
+when `configure` fails to locate the library — most autotools-based projects
+find libraries via `pkg-config` (which Coral also configures via
+`$PKG_CONFIG_PATH`) or their own `--with-*` flags.
+
+#### pkg-config
+
+Coral sets `PKG_CONFIG_PATH` to `$PREFIX/lib/pkgconfig` so that `configure`
+scripts using `pkg-config` can find `.pc` files from Coral-installed packages.
+This is the preferred mechanism for library discovery — most modern autotools
+and meson projects use `pkg-config` and will automatically pick up the correct
+`-I`, `-L`, and `-l` flags from `.pc` files.
+
+### Install Scripts
+
+Packages can include scripts that run during installation, removal, and upgrade. Create a `.SCRIPTS/` directory in the package:
+
+```
+.SCRIPTS/
+├── pre-install.sh    # Runs before files are installed ($VERSION)
+├── post-install.sh   # Runs after files are installed ($VERSION)
+├── pre-remove.sh     # Runs before files are removed ($VERSION)
+├── post-remove.sh    # Runs after files are removed ($VERSION)
+├── pre-upgrade.sh    # Runs before upgrade ($OLD_VERSION $NEW_VERSION)
+└── post-upgrade.sh   # Runs after upgrade ($OLD_VERSION $NEW_VERSION)
+```
+
+Scripts are executed with `zsh`. Version arguments are passed as positional parameters (`$1`, `$2`).
+
+**Hook behavior:**
+- Pre-hooks (`pre-install`, `pre-remove`, `pre-upgrade`) abort the operation if they exit non-zero
+- Post-hooks (`post-install`, `post-remove`, `post-upgrade`) log a warning but don't abort on failure
+
+**Upgrade flow:** When installing a newer version of an already-installed package, Coral runs: `pre-upgrade` → remove old files → install new files → `post-upgrade`.
+
+Example post-install.sh:
+```bash
+#!/usr/bin/zsh
+# Update font cache after installing fonts
+fc-cache -f
+```
+
+Example pre-upgrade.sh:
+```bash
+#!/usr/bin/zsh
+OLD_VERSION=$1
+NEW_VERSION=$2
+# Stop service before upgrade
+svcadm disable myservice
+```
+
+### Config Files
+
+To mark files as configuration files (preserved during upgrades), create a `.CONFIG` file listing them:
+
+```
+/etc/mypackage/config.conf
+/etc/mypackage/settings.toml
+```
+
+Config files use checksum-based protection during upgrades:
+- **Fresh install**: config file is installed normally
+- **Upgrade, user has not modified config**: new version overwrites cleanly
+- **Upgrade, user has modified config**: new version is installed as `.pkg-new`, user's file is preserved
+
+On removal, user-modified config files are preserved (not deleted).
+
+### Building the Package
+
+```bash
+coral build mypackage
+```
+
+This will:
+1. Resolve dependencies recursively (build uninstalled deps from source first)
+2. Download sources (with automatic mirror failover)
+3. Verify checksums
+4. Extract sources
+5. Apply patches
+6. Run build script
+7. Compress man pages (if `compress_man = true`)
+8. Create package archive
+9. Clean up build directories
+
+If a dependency is not installed, Coral builds it from source automatically using a topological sort of the full dependency tree. Circular dependencies are detected and reported.
+
+#### Build Command Options
+
+| Option | Description |
+|--------|-------------|
+| `--noclean` / `-n` | Skip cleanup of work and staging directories after build |
+| `--clean` / `-c` | Force cleanup even if config says to keep directories |
+| `--quiet` / `-q` | Hide subprocess output (still logged to file) |
+| `--verbose` / `-v` | Show all output (overrides quiet mode) |
+| `-y` | Skip confirmation prompts |
+| `--force` | Force rebuild even if package exists |
+
+Example: Keep build artifacts for debugging:
+```bash
+coral build --noclean mypackage
+```
+
+#### Build Cleanup Behavior
+
+After a successful build, Coral cleans up:
+- **Work directory** (`/var/tmp/coral/work/<package>`): Extracted sources and build files
+- **Staging directory** (`/var/tmp/coral/pkg/<package>`): Package staging area
+
+Cleanup behavior can be controlled via:
+1. Command-line flags (`--noclean`, `--clean`)
+2. Configuration file settings (`clean_work`, `clean_pkg`)
+
+Downloaded source archives are preserved by default in `/var/lib/coral/sources/`.
+
+#### Build Logging
+
+By default, Coral logs all build output to a file in the work directory:
+
+```
+/var/tmp/coral/work/<package>/coral-build-<package>-<timestamp>.log
+```
+
+The log captures:
+- All Coral status messages (download, extract, patch, build, package)
+- Full subprocess output (build script, patch commands)
+- Commands executed
+
+Log files are cleaned up with the work directory unless `--noclean` is specified.
+
+Use `--quiet` to hide subprocess output from the terminal while still logging everything:
+```bash
+coral build --quiet vim    # Clean output, full log file
+coral build --verbose vim  # Show all output (overrides quiet)
+```
+
+---
+
+## Package Groups
+
+Groups allow installing multiple related packages with a single command.
+
+### Creating a Group
+
+Create a TOML file in `/usr/zports/groups/`:
+
+```toml
+# /usr/zports/groups/base-devel.toml
+
+[group]
+name = "base-devel"
+description = "Base development tools"
+packages = [
+    "gcc",
+    "gmake",
+    "binutils",
+    "autoconf",
+    "automake",
+    "libtool",
+    "pkg-config"
+]
+```
+
+### Installing a Group
+
+Use the `@` prefix:
+```bash
+coral install @base-devel
+```
+
+This expands to all packages in the group and installs them with dependency resolution.
+
+### Listing Groups
+
+Groups are stored as `.toml` files in `/usr/zports/groups/`. List them with:
+```bash
+ls /usr/zports/groups/
+```
+
+---
+
+## Alternative Dependencies
+
+Alternatives allow a package to depend on one of several options. This is useful when multiple packages can satisfy a dependency.
+
+### Version Constraints
+
+Dependencies can include version constraints using operators:
+
+```toml
+[dependencies]
+runtime = [
+    "libfoo>=1.0",        # Version 1.0 or newer
+    "libbar<=2.0",        # Version 2.0 or older
+    "libqux=3.1.0",       # Exactly version 3.1.0
+    "libbaz>0.9",         # Strictly newer than 0.9
+    "libold<2.0",         # Strictly older than 2.0
+]
+```
+
+If an installed package doesn't satisfy the version constraint, Coral includes it in the install order for upgrade.
+
+### Specifying Alternatives
+
+In `package.toml`, use the colon syntax:
+
+```toml
+[dependencies]
+runtime = [
+    "libfoo",
+    "editor:vim,nano,emacs",     # Any of these editors
+    "database:postgresql,mysql"  # Any of these databases
+]
+```
+
+Format: `base_name:option1,option2,option3`
+
+### How Alternatives Work
+
+1. During dependency resolution, Coral checks if any alternative is installed
+2. If one is installed, the dependency is satisfied
+3. If none are installed, the first option is automatically selected
+4. Users can override by pre-installing their preferred option
+
+### Providing Alternatives
+
+A package can declare that it provides certain alternatives:
+
+```toml
+[package]
+name = "vim"
+alternatives = ["editor", "vi"]
+```
+
+When another package depends on "editor", vim will satisfy that dependency.
+
+---
+
+## Creating Repositories
+
+Repositories distribute pre-built binary packages.
+
+### Repository Structure
+
+```
+/path/to/repo/
+├── repo.toml           # Repository manifest
+├── repo.toml.sig       # Manifest signature (optional)
+└── packages/           # Binary packages
+    ├── vim-9.0-1.x86_64.pkg.tar.xz
+    ├── vim-9.0-1.x86_64.pkg.tar.xz.sig
+    └── ...
+```
+
+### Initializing a Repository
+
+```bash
+coral repo init /path/to/repo
+```
+
+This creates the directory structure and an empty manifest.
+
+### Adding Packages
+
+Copy built packages to the `packages/` directory:
+```bash
+cp /var/lib/coral/packages/*.pkg.tar.xz /path/to/repo/packages/
+```
+
+### Rebuilding the Manifest
+
+After adding packages, rebuild the manifest:
+```bash
+coral repo rebuild /path/to/repo
+```
+
+This scans all packages and generates `repo.toml`:
+
+```toml
+[repository]
+name = "myrepo"
+description = "My Package Repository"
+url = "https://repo.example.com"
+
+[packages.vim]
+version = "9.0"
+release = 1
+description = "Vi IMproved"
+license = "Vim"
+category = "base"
+file = "vim-9.0-1.x86_64.pkg.tar.xz"
+sha256 = "abc123..."
+size = 12345678
+compressed = 2345678
+depends = ["ncurses", "libfoo"]
+
+[packages.nano]
+# ... more packages
+```
+
+### Signing the Repository
+
+```bash
+coral repo sign /path/to/repo
+```
+
+This signs `repo.toml` with your private key, creating `repo.toml.sig`.
+
+### Configuring Clients
+
+Create a repository configuration file on client systems:
+
+```toml
+# /etc/coral/repos.d/myrepo.repo
+
+[repository]
+name = "myrepo"
+url = "https://repo.example.com"
+priority = 100
+enabled = true
+signed = true
+keyid = "myrepo-key"
+```
+
+### Refreshing Repository Data
+
+```bash
+coral repo refresh
+```
+
+### Mirroring
+
+Export a repository for offline use:
+```bash
+coral repo export /path/to/repo /path/to/output
+```
+
+---
+
+## Package Signing
+
+Coral uses Ed25519 signatures for package and repository verification.
+
+### Key Management
+
+#### Initialize Keyring
+
+```bash
+coral key init
+```
+
+Creates `/etc/coral/keys/` directory.
+
+#### Generate a Signing Key
+
+```bash
+coral key generate mykey
+```
+
+Creates:
+- `/etc/coral/keys/mykey.key` (private key - keep secure!)
+- `/etc/coral/keys/mykey.pub` (public key - distribute this)
+
+#### List Keys
+
+```bash
+coral key list
+```
+
+#### Export Public Key
+
+```bash
+coral key export mykey > mykey.pub
+```
+
+#### Import a Public Key
+
+```bash
+coral key import mykey.pub repokey
+```
+
+#### Trust a Key
+
+```bash
+coral key trust repokey
+```
+
+Adds the key to `/var/lib/coral/trusted-keys/`.
+
+#### Revoke Trust
+
+```bash
+coral key revoke repokey
+```
+
+### Signing Packages
+
+When building a package, sign it:
+```bash
+coral build mypackage
+coral key sign /var/lib/coral/packages/mypackage-1.0-1.pkg.tar.xz mykey
+```
+
+### Signing Repositories
+
+```bash
+coral repo sign /path/to/repo
+```
+
+Uses the first available private key, or specify one:
+```bash
+coral repo sign /path/to/repo --key mykey
+```
+
+### Verification
+
+Enable mandatory verification in `/etc/coral/coral.conf`:
+
+```toml
+[security]
+require_signed_repos = true
+require_signed_packages = true
+```
+
+When enabled:
+- Repository manifests must have valid signatures
+- Packages must have valid signatures
+- Signatures must be from trusted keys
+
+### Verification Process
+
+1. Download package and signature
+2. Find matching public key in trusted keys
+3. Verify signature using Ed25519
+4. Reject if verification fails (when required)
+
+---
+
+## Database Management
+
+Coral maintains MsgPack-based indexes for fast package and file lookups. The source of truth is always the installed package database (`/var/lib/coral/installed/`); indexes can be safely rebuilt at any time.
+
+### Database Files
+
+| File | Purpose |
+|------|---------|
+| `/var/lib/coral/db/packages.db` | Indexed package metadata (name, version, description) |
+| `/var/lib/coral/db/files.db` | File-to-package mapping for O(log N) ownership lookups |
+| `/var/lib/coral/db/ports.db` | Ports index for fast search/list without parsing TOML from disk |
+
+### Initializing the Database
+
+After initial system setup, initialize the database:
+```bash
+coral db init
+```
+
+This creates the index files based on currently installed packages.
+
+### Checking Database Status
+
+View the current database status:
+```bash
+coral db status
+```
+
+Output shows:
+- Index location
+- Number of packages indexed
+- Number of files indexed
+- Number of ports indexed
+
+### Rebuilding Indexes
+
+If indexes become corrupted or out of sync (e.g., after manual edits to `/var/lib/coral/installed/`), rebuild them:
+```bash
+coral db rebuild
+```
+
+This scans all installed packages and regenerates both indexes from scratch.
+
+### How Indexes Work
+
+**packages.db**: Stores package metadata in MsgPack format, encoded as Base64. Contains name, version, release, description, install date, and file count for each installed package.
+
+**files.db**: Maps file paths to owning packages. Files are sorted alphabetically, enabling O(log N) binary search lookups. This dramatically speeds up `coral owner` queries.
+
+### Automatic Index Updates
+
+Indexes are automatically updated when:
+- A package is installed (`coral install`)
+- A package is removed (`coral remove`)
+- The ports tree is synced (`coral sync` — rebuilds ports.db)
+
+You do not need to manually rebuild indexes during normal operation.
+
+### Performance
+
+| Operation | Without Index | With Index |
+|-----------|---------------|------------|
+| `coral list` (100 packages) | ~500ms | ~10ms |
+| `coral owner /usr/bin/vim` (50K files) | ~2000ms | <10ms |
+
+### Using with Alternate Root
+
+All database commands support the `--root` flag for chroot environments:
+```bash
+coral --root /mnt/system db init
+coral --root /mnt/system db status
+coral --root /mnt/system db rebuild
+```
+
+---
+
+## Port Development Tools
+
+The `coral ports` command provides tooling for port developers.
+
+### Scaffolding a New Port
+
+Create a new port with boilerplate files:
+```bash
+coral ports new hammerhead/my-port
+```
+
+This creates:
+- `{ports_dir}/hammerhead/my-port/package.toml` — package metadata template
+- `{ports_dir}/hammerhead/my-port/build.sh` — build script stub
+
+The generated `package.toml` includes all standard sections with placeholder values. Edit it to fill in your package's details, then test with:
+```bash
+coral build hammerhead/my-port
+```
+
+### Ports Index Cache
+
+The ports index (`ports.db`) caches metadata from all ports so that commands like `coral search`, `coral list --available`, and `coral depends --reverse` don't need to parse every `package.toml` from disk.
+
+Check index status:
+```bash
+coral ports cache status
+```
+
+Manually rebuild the index:
+```bash
+coral ports cache rebuild
+```
+
+The index is automatically rebuilt by `coral sync`, `coral db init`, and `coral db rebuild`.
+
+### Dynamic Categories
+
+Categories are discovered dynamically by scanning the ports directory. Any subdirectory under the ports tree (excluding infrastructure directories like `pkgs/` and `groups/`) is treated as a category. Adding a new category directory is all that's needed — no code changes required.
+
+---
+
+## Configuration Reference
+
+### /etc/coral/coral.conf
+
+```toml
+[general]
+# Target architecture
+arch = "x86_64"
+
+# Parallel build jobs (0 = auto-detect CPU count)
+jobs = 0
+
+# Ask for confirmation before operations
+confirm = true
+
+# Enable colored output
+color = true
+
+[paths]
+# Installation prefix (exported as $PREFIX to build scripts)
+prefix = "/usr/local"
+
+# System configuration directory (exported as $SYSCONFDIR to build scripts)
+sysconfdir = "/etc"
+
+# Ports tree location
+ports = "/usr/ports"
+
+# Built packages cache
+packages = "/var/lib/coral/packages"
+
+# Downloaded sources cache
+sources = "/var/lib/coral/sources"
+
+# Installed package database
+database = "/var/lib/coral/installed"
+
+# Build work directory
+work = "/var/tmp/coral/work"
+
+# Package staging directory
+pkg = "/var/tmp/coral/pkg"
+
+[build]
+# Default C compiler flags
+cflags = "-O2 -pipe"
+
+# Default C++ compiler flags
+cxxflags = "-O2 -pipe"
+
+# Default make flags
+makeflags = ""
+
+# Strip binaries
+strip = true
+
+# Compress man pages
+compress_man = true
+
+# Fall back to building from source if binary not available
+fallback_to_source = true
+
+# Log build output to file (in work directory)
+logging = true
+
+# Hide subprocess output from terminal (still logged)
+quiet = false
+
+[network]
+# Download timeout in seconds
+timeout = 30
+
+# Number of retry attempts
+retries = 3
+
+# Use proxy (reads http_proxy/https_proxy env vars)
+use_proxy = false
+
+[sync]
+# Auto-refresh repository metadata
+auto_refresh = false
+
+[cache]
+# Keep downloaded packages after install
+keep_packages = true
+
+# Keep downloaded sources after build
+keep_sources = true
+
+# Clean work directory after successful build (part of [build] section)
+clean_work = true
+
+# Clean package staging directory after successful build
+clean_pkg = true
+
+[security]
+# Require signed repository manifests
+require_signed_repos = false
+
+# Require signed packages
+require_signed_packages = false
+```
+
+### Repository Configuration
+
+Repository files in `/etc/coral/repos.d/*.repo`:
+
+```toml
+[repository]
+# Repository name (used in messages)
+name = "zygaena-main"
+
+# Base URL for the repository
+url = "https://repo.zygaena.org/main"
+
+# Priority (lower = higher priority)
+priority = 100
+
+# Enable/disable this repository
+enabled = true
+
+# Repository is signed
+signed = true
+
+# Key ID for signature verification
+keyid = "zygaena-main"
+```
+
+---
+
+## Troubleshooting
+
+### Common Issues
+
+**"Package not found"**
+- Check if the port exists: `ls /usr/zports/*/packagename`
+- Refresh repository metadata: `coral repo refresh`
+- Check repository configuration in `/etc/coral/repos.d/`
+
+**"Signature verification failed"**
+- Import the repository's public key: `coral key import`
+- Trust the key: `coral key trust keyname`
+- Check key hasn't been revoked
+
+**"Dependency resolution failed"**
+- Check for circular dependencies
+- Verify all dependencies are available
+- Try installing dependencies manually
+
+**"Build failed"**
+- Check build.sh for errors
+- Verify build dependencies are installed
+- Check logs in `/var/tmp/coral/work/`
+
+### Debug Mode
+
+Enable verbose output:
+```bash
+coral -v install vim
+```
+
+Dry run (show what would be done):
+```bash
+coral -n install vim
+```
+
+---
+
+## Quick Reference
+
+| Command | Description |
+|---------|-------------|
+| `coral install pkg` | Install package |
+| `coral install @group` | Install package group |
+| `coral install -n pkg` | Dry run (show what would happen) |
+| `coral remove pkg` | Remove package |
+| `coral upgrade` | Upgrade all packages |
+| `coral list` | List installed packages |
+| `coral info pkg` | Show package info |
+| `coral files pkg` | List package files |
+| `coral owner /path` | Find file owner |
+| `coral search term` | Search packages |
+| `coral depends pkg` | Show dependencies |
+| `coral verify pkg` | Verify package integrity |
+| `coral build pkg` | Build from port (no install) |
+| `coral build --install pkg` | Build and install |
+| `coral build --noclean pkg` | Build, keep work directories |
+| `coral build --quiet pkg` | Build with quiet output |
+| `coral sync` | Update ports tree |
+| `coral outdated` | Show available updates |
+| `coral repo list` | List repositories |
+| `coral repo refresh` | Refresh repo metadata |
+| `coral key list` | List signing keys |
+| `coral key generate name` | Generate new key |
+| `coral clean status` | Show build artifact status |
+| `coral clean all` | Clean all build artifacts |
+| `coral clean --force all` | Clean all, override cache protection |
+| `coral db init` | Initialize database indexes |
+| `coral db status` | Show database status |
+| `coral db rebuild` | Rebuild indexes from installed packages |
+| `coral ports new cat/name` | Scaffold a new port |
+| `coral ports cache status` | Show ports index status |
+| `coral ports cache rebuild` | Rebuild ports index |
+
+---
+
+## License
+
+Coral is part of the Zygaena project.
+
+Copyright (C) 2025 Leafscale, LLC
+Licensed under BSD-2-Clause
+
+https://www.leafscale.com
diff -r 000000000000 -r d0a96d73e9e1 docs/MSGPACK_DATABASE_PLAN.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/MSGPACK_DATABASE_PLAN.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,309 @@
+# Implementation Plan: MsgPack Package Database Indexes
+
+**Date:** 2026-01-29
+**Status:** Planned
+**Target:** Coral v0.2
+
+## Overview
+
+Implement the MsgPack-based package database indexes that were planned as the key feature of Coral v0.2. Currently `src/core/pkgdb.reef` is entirely stubs that fall back to slow file scanning. This causes:
+
+- `coral list` - O(N) TOML file reads
+- `coral owner` - O(N*M) full scan of all packages' file lists (CRITICAL bottleneck)
+- `coral info` - O(1) but requires TOML parsing
+
+## Target Performance
+
+| Command | Current | With MsgPack |
+|---------|---------|--------------|
+| `coral list` (100 pkgs) | ~500ms | ~10ms |
+| `coral owner /usr/bin/vim` (50K files) | ~2000ms | ~1ms |
+| `coral info vim` | ~50ms | ~5ms |
+
+---
+
+## File Format Specifications
+
+### packages.db (MsgPack + Base64)
+
+```
+Structure:
+{
+  "version": 1,
+  "generated": "2026-01-29T...",
+  "package_count": N,
+  "packages": [
+    { "name": "vim", "version": "9.1", "release": 1,
+      "description": "...", "installed_date": "...", "file_count": 423 },
+    ...
+  ]
+}
+```
+
+### files.db (MsgPack + Base64)
+
+```
+Structure:
+{
+  "version": 1,
+  "generated": "2026-01-29T...",
+  "file_count": M,
+  "files": {
+    "usr/bin/vim": "vim",
+    "usr/lib/libz.so": "zlib",
+    ...
+  }
+}
+```
+
+**Note:** Base64 encoding required because Reef's `file.readFile()`/`file.writeFile()` work with strings.
+
+---
+
+## Implementation Phases
+
+### Phase 1: Foundation (in pkgdb.reef)
+
+Add new types:
+
+```reef
+type PackagesIndex = struct
+    version: int
+    generated: string
+    package_count: int
+    entries: [PkgIndexEntry]
+    loaded: bool
+end PackagesIndex
+
+type FilesIndex = struct
+    version: int
+    generated: string
+    file_count: int
+    file_paths: [string]    // Sorted for binary search
+    owners: [string]        // Parallel array - owner at same index
+    loaded: bool
+end FilesIndex
+```
+
+Add helper functions:
+- `base64_encode(buf: [int], length: int): string`
+- `base64_decode(encoded: string, buf: [int]): int`
+- `estimate_packages_buffer_size(pkg_count: int): int`
+- `estimate_files_buffer_size(file_count: int): int`
+
+### Phase 2: Serialization
+
+Implement MsgPack serialization:
+
+```reef
+fn serialize_packages_db(root: string): [int]
+    // 1. Get list of installed packages from database module
+    // 2. Allocate buffer based on count estimate
+    // 3. Pack map header (4 keys: version, generated, package_count, packages)
+    // 4. Pack array of package entries
+    // Returns packed buffer
+end serialize_packages_db
+
+fn serialize_files_db(root: string): [int]
+    // 1. Get all packages, collect all files with owners
+    // 2. Sort file paths for binary search later
+    // 3. Allocate buffer
+    // 4. Pack map header + files map
+    // Returns packed buffer
+end serialize_files_db
+```
+
+Implement `rebuild_indexes_rooted()`:
+```reef
+fn rebuild_indexes_rooted(root: string): bool
+    // 1. Serialize packages.db, base64 encode, write file
+    // 2. Serialize files.db, base64 encode, write file
+    // 3. Return success
+end rebuild_indexes_rooted
+```
+
+### Phase 3: Deserialization
+
+Implement loading functions:
+
+```reef
+fn load_packages_index(root: string): PackagesIndex
+    // 1. Read base64 string from packages.db file
+    // 2. Decode to binary buffer
+    // 3. Unpack MsgPack structure
+    // 4. Return populated PackagesIndex
+end load_packages_index
+
+fn load_files_index(root: string): FilesIndex
+    // 1. Read base64 string from files.db file
+    // 2. Decode to binary buffer
+    // 3. Unpack MsgPack map into parallel arrays
+    // 4. Return populated FilesIndex (already sorted from serialize)
+end load_files_index
+
+fn binary_search_file(index: FilesIndex, path: string): string
+    // O(log N) binary search in sorted file_paths array
+    // Return owner or empty string
+end binary_search_file
+```
+
+### Phase 4: Update Public API
+
+Replace stub implementations:
+
+```reef
+fn list_packages(names: [string], versions: [string], max_count: int): int
+    let index = load_packages_index("")
+    if not index.loaded
+        return fallback_list_packages(names, versions, max_count)
+    end if
+    // Copy from index to arrays
+end list_packages
+
+fn find_file_owner(file_path: string): string
+    let index = load_files_index("")
+    if not index.loaded
+        return database.find_owner(file_path)  // Fallback
+    end if
+    return binary_search_file(index, normalize_path(file_path))
+end find_file_owner
+
+fn get_indexed_package(name: string): PkgIndexEntry
+    let index = load_packages_index("")
+    if not index.loaded
+        return fallback_get_indexed_package(name)
+    end if
+    // Search index for package by name
+end get_indexed_package
+```
+
+Add rooted variants:
+- `find_file_owner_rooted(file_path: string, root: string): string`
+
+### Phase 5: Command Integration
+
+**install.reef** - After registering package:
+```reef
+// Rebuild indexes after install
+pkgdb.rebuild_indexes_rooted(root_path)
+```
+
+**remove.reef** - After unregistering package:
+```reef
+// Rebuild indexes after remove
+pkgdb.rebuild_indexes_rooted(root_path)
+```
+
+**owner.reef** - Use indexed lookup:
+```reef
+// Change from database.find_owner() to pkgdb.find_file_owner()
+```
+
+---
+
+## Files to Modify
+
+| File | Changes |
+|------|---------|
+| `src/core/pkgdb.reef` | Main implementation - add types, serialization, deserialization, binary search |
+| `src/commands/install.reef` | Call `rebuild_indexes_rooted()` after registration |
+| `src/commands/remove.reef` | Call `rebuild_indexes_rooted()` after unregistration |
+| `src/commands/owner.reef` | Use `pkgdb.find_file_owner()` instead of `database.find_owner()` |
+
+---
+
+## Key Implementation Details
+
+### Base64 Encoding (required for file I/O)
+
+Reef's `encoding.base64` module exists in reef-stdlib. Use:
+```reef
+import encoding.base64
+let encoded = base64.base64_encode(binary_string)
+let decoded = base64.base64_decode(encoded)
+```
+
+**Issue:** base64 module works with strings, but MsgPack produces `[int]` buffer. Need conversion helpers.
+
+### MsgPack Usage (from reef-stdlib/encoding/msgpack.reef)
+
+```reef
+import encoding.msgpack
+
+// Packing
+let buf = msgpack.msgpack_alloc_buffer(size)
+let pos = 0
+pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos)
+pos = pos + msgpack.msgpack_pack_string("version", buf, pos)
+pos = pos + msgpack.msgpack_pack_int(1, buf, pos)
+// ... etc
+
+// Unpacking
+let map_len = msgpack.msgpack_unpack_map_len(buf, offset)
+offset = offset + msgpack.msgpack_value_size(buf, offset)
+// ... iterate through map entries
+```
+
+### Buffer to String Conversion
+
+Need helper to convert `[int]` buffer to string for base64:
+```reef
+fn buffer_to_string(buf: [int], length: int): string
+    mut result = ""
+    mut i = 0
+    while i < length
+        result = result + chr(buf[i])
+        i = i + 1
+    end while
+    return result
+end buffer_to_string
+```
+
+---
+
+## Verification
+
+### Test Commands
+
+```bash
+# Initialize database (should create packages.db and files.db)
+sudo coral --root /tmp/coral-chroot db init
+ls -la /tmp/coral-chroot/var/lib/coral/db/
+
+# Verify files exist
+file /tmp/coral-chroot/var/lib/coral/db/packages.db
+file /tmp/coral-chroot/var/lib/coral/db/files.db
+
+# Build and install a test package
+coral build core/zlib
+sudo coral --root /tmp/coral-chroot install zlib
+
+# Verify index updated
+sudo coral --root /tmp/coral-chroot db status
+
+# Test fast list
+time sudo coral --root /tmp/coral-chroot list
+
+# Test fast owner lookup
+time sudo coral --root /tmp/coral-chroot owner /usr/lib/libz.so
+
+# Test rebuild from scratch
+sudo rm /tmp/coral-chroot/var/lib/coral/db/*.db
+sudo coral --root /tmp/coral-chroot db rebuild
+```
+
+### Performance Validation
+
+- `coral list` with 100 packages should complete in <50ms
+- `coral owner` should complete in <10ms regardless of file count
+- Index files should be created after install/remove
+
+---
+
+## Design Decisions
+
+1. **Full Rebuild on Changes:** Each install/remove triggers a complete index rebuild. Simpler, always consistent, acceptable performance for typical package counts (<500 packages).
+
+2. **Trust the Index:** Commands trust the index is correct. If indexes get out of sync (e.g., manual edits to installed/ directory), user runs `coral db rebuild` to fix.
+
+3. **No Caching:** Reload index from disk on each command invocation. Keeps implementation simple and avoids stale memory issues.
diff -r 000000000000 -r d0a96d73e9e1 docs/PACKAGE_FORMAT_V2_DESIGN.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/PACKAGE_FORMAT_V2_DESIGN.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,408 @@
+# Design: Package Format v2
+
+**Date:** 2026-03-07
+**Status:** Draft
+**Target:** Coral v0.3
+**Authors:** Zygaena Team
+
+## Overview
+
+Redesign of the Coral package internal format to eliminate the rsync dependency,
+add a rich file manifest for install/remove/verify operations, embed dependency
+metadata in packages, fix config file handling, and clean up the scripting model.
+
+These changes are possible now because Reef v0.5.0 provides native filesystem
+operations (`fs.stat`, `fs.perm`, `fs.link`, `fs.ops`) that replace the need for
+shell-outs to rsync, cp, chmod, chown, and ln.
+
+No package has shipped to production yet, so there is no backward-compatibility
+constraint.
+
+---
+
+## 1. Package Archive Structure
+
+The archive format remains `.pkg.tar.xz` (gtar + xz). The internal layout changes:
+
+```
+name#version-release.pkg.tar.xz
+  .PKGINFO              # TOML — package metadata + dependencies
+  .MANIFEST             # mtree-format file inventory (replaces .FOOTPRINT)
+  .CONFIG               # list of config-protected paths (unchanged)
+  .SCRIPTS/             # optional hook scripts (zsh only)
+      pre-install.sh
+      post-install.sh
+      pre-remove.sh
+      post-remove.sh
+      pre-upgrade.sh
+      post-upgrade.sh
+  usr/
+  etc/
+  ...                   # payload files at their install paths
+```
+
+### Changes from v1
+
+| Item | v1 | v2 |
+|------|----|----|
+| File manifest | `.FOOTPRINT` (flat path list) | `.MANIFEST` (mtree with metadata) |
+| Dependencies | not in package | `[dependencies]` in `.PKGINFO` |
+| Script hooks | 4 (install/remove) | 6 (+ upgrade pair) |
+| Script runners | `.reef`, `.sh`, bare | `.sh` (zsh) and bare only |
+| Install method | `rsync -a` | `fs.ops` driven by manifest |
+
+---
+
+## 2. .PKGINFO — Package Metadata
+
+TOML format. Adds a `[dependencies]` section with optional version constraints.
+
+```toml
+[package]
+name = "openssl"
+version = "3.2.1"
+release = 1
+description = "TLS/SSL and crypto library"
+url = "https://www.openssl.org"
+license = "Apache-2.0"
+maintainer = "admin@zygaena.org"
+arch = "x86_64"
+
+[dependencies]
+runtime = ["zlib", "libc"]
+build = ["perl", "makedepend"]
+```
+
+### Version constraints
+
+Runtime and build dependencies support optional version operators:
+
+```toml
+runtime = ["zlib>=1.3", "libc"]
+```
+
+Supported operators: `>=`, `<=`, `>`, `<`, `=`. No operator means "any version".
+
+Most dependencies will be unversioned. Version constraints are added only when a
+specific ABI or feature is required (e.g., a soname bump, a new API entry point).
+Exact version pinning (`=`) is discouraged — it creates rebuild cascades.
+
+The port's `package.toml` remains the authoritative source. The `[dependencies]`
+section in `.PKGINFO` is a copy embedded at build time so standalone packages are
+self-describing without the ports tree.
+
+### Generation
+
+`generate_pkginfo()` in `build.reef` writes the `[dependencies]` section by
+reading `ctx.port.deps.runtime` and `ctx.port.deps.build`.
+
+---
+
+## 3. .MANIFEST — mtree File Inventory
+
+Replaces `.FOOTPRINT`. Uses mtree-style format: one line per filesystem entry,
+path first, then space-separated `key=value` attributes.
+
+### Format
+
+```
+#mtree
+./usr/bin type=dir mode=0755 uname=root gname=root
+./usr/bin/passwd type=file mode=4755 uname=root gname=sys sha256=a1b2c3d4e5f6...
+./usr/bin/zlib-flate type=file mode=0755 uname=root gname=root sha256=d4e5f6a7...
+./usr/include type=dir mode=0755 uname=root gname=root
+./usr/include/zconf.h type=file mode=0644 uname=root gname=root sha256=e5f6a7b8...
+./usr/lib type=dir mode=0755 uname=root gname=root
+./usr/lib/libz.a type=file mode=0644 uname=root gname=root sha256=9c0d1e2f...
+./usr/lib/libz.so type=link uname=root gname=root link=libz.so.1
+./usr/lib/libz.so.1 type=link uname=root gname=root link=libz.so.1.3.1
+./usr/lib/libz.so.1.3.1 type=file mode=0755 uname=root gname=root sha256=f0a1b2...
+```
+
+### Fields
+
+| Key | Required | Values | Notes |
+|-----|----------|--------|-------|
+| `type` | always | `file`, `dir`, `link` | First attribute after path |
+| `mode` | files, dirs | 4-digit octal | Includes setuid/setgid/sticky |
+| `uname` | always | user name | String, not numeric uid |
+| `gname` | always | group name | String, not numeric gid |
+| `sha256` | files only | 64-char hex | For install verification and `coral verify` |
+| `link` | symlinks only | target path | Relative or absolute link target |
+
+### Sort order
+
+Entries are sorted lexicographically by path. This ensures:
+- Directories appear before their contents (install order: top-down)
+- Removal in reverse order processes files before their parent directories
+
+### Extensibility
+
+Unknown `key=value` pairs are ignored by the parser. Future attributes (e.g.,
+`xattr=...`, `acl=...`, `flags=...`) can be added without breaking existing
+Coral versions. However, ACLs and extended attributes are better handled in
+post-install scripts where policy decisions belong.
+
+### Metadata exclusion
+
+`.PKGINFO`, `.MANIFEST`, `.CONFIG`, and `.SCRIPTS/` are not listed in the
+manifest. They are package metadata, not payload files.
+
+### Generation
+
+`generate_manifest()` in `build.reef` replaces `generate_footprint()`. It walks
+the staging directory using `fs.stat` and `fs.perm` to read type, mode, owner,
+and group for each entry. File checksums are computed with `checksum.sha256_file()`.
+Symlink targets are read with `fs.link.readlink()`.
+
+### Parser module
+
+New module: `util/mtree.reef`
+
+```
+export
+    type ManifestEntry
+    fn parse_manifest(content: string, entries: [ManifestEntry], max: int): int
+    fn generate_manifest(pkg_dir: string): string
+    fn entry_type(entry: ManifestEntry): string
+    fn entry_path(entry: ManifestEntry): string
+    fn entry_mode(entry: ManifestEntry): int
+    fn entry_sha256(entry: ManifestEntry): string
+    fn entry_link(entry: ManifestEntry): string
+    fn entry_uname(entry: ManifestEntry): string
+    fn entry_gname(entry: ManifestEntry): string
+end export
+```
+
+---
+
+## 4. Install Method — Replacing rsync
+
+### Current flow (v1)
+
+```
+extract .pkg.tar.xz to temp dir
+  → find symlinks, remove conflicts in root
+  → rsync -a --exclude=metadata temp_dir/ root/
+```
+
+### New flow (v2)
+
+```
+extract .pkg.tar.xz to temp dir
+  → parse .MANIFEST into entry array
+  → for each entry (in manifest order):
+      if type=dir:
+          fs.ops.create_dir_all(root + path)        -- create if missing
+          fs.perm.chmod(root + path, mode)           -- set permissions
+          fs.perm.chown(root + path, uname, gname)   -- set ownership
+      if type=file:
+          check if config-protected (see section 6)
+          if config file with user modifications → install as .pkg
+          else:
+              remove any existing conflicting path (file, symlink, or dir)
+              fs.ops.copy_file_preserve(src, root + path)
+              fs.perm.chmod(root + path, mode)
+              fs.perm.chown(root + path, uname, gname)
+              verify sha256 matches manifest
+      if type=link:
+          remove any existing conflicting path
+          fs.link.symlink(link_target, root + path)
+```
+
+### Advantages over rsync
+
+- **Zero external dependencies** — no rsync required during bootstrap
+- **Manifest-driven** — every file placed is accounted for, no silent extras
+- **Per-file verification** — sha256 checked on install, not just blindly copied
+- **Config protection integrated** — checked inline during install, not as a
+  separate pass
+- **Explicit permissions** — set from manifest, not inherited from build host
+  filesystem quirks
+
+### Error handling
+
+If any file operation fails, installation stops and returns an error. Partial
+installs are recorded so `coral remove` can clean up. This is safer than rsync's
+`exit code 23` (partial transfer accepted).
+
+---
+
+## 5. Scripting Model
+
+### Hooks
+
+Six hooks, all executed with `zsh`:
+
+| Hook | Arguments | When | Abort on failure |
+|------|-----------|------|------------------|
+| `pre-install.sh` | `$VERSION` | Before files installed (fresh install) | Yes |
+| `post-install.sh` | `$VERSION` | After files installed (fresh install) | No* |
+| `pre-remove.sh` | `$VERSION` | Before files removed (full remove) | Yes |
+| `post-remove.sh` | `$VERSION` | After files removed (full remove) | No* |
+| `pre-upgrade.sh` | `$OLD_VERSION $NEW_VERSION` | Before files replaced (upgrade) | Yes |
+| `post-upgrade.sh` | `$OLD_VERSION $NEW_VERSION` | After new files installed (upgrade) | No* |
+
+*Post-scripts log failures as warnings but do not roll back, since the file
+operation has already completed.
+
+### Execution
+
+All scripts are invoked as:
+
+```
+zsh "/path/to/.SCRIPTS/hook-name.sh" [args...]
+```
+
+The `.reef` script type is removed. Requiring the Reef compiler at install time
+creates a bootstrap dependency that cannot be satisfied when building the base OS.
+
+Bare executables (no extension) are still supported — Coral executes them
+directly, relying on a shebang line or ELF header.
+
+### Script search order
+
+For each hook, Coral looks for (in order):
+1. `hook-name.sh` — executed with `zsh`
+2. `hook-name` — executed directly (must be executable with valid shebang or ELF)
+
+If neither exists, the hook is silently skipped (not an error).
+
+### Upgrade flow
+
+```
+pre-upgrade.sh  $OLD_VERSION  $NEW_VERSION    # abort if non-zero
+  → remove old files (driven by installed manifest)
+  → install new files (driven by new manifest)
+post-upgrade.sh  $OLD_VERSION  $NEW_VERSION   # warn if non-zero
+```
+
+If no `pre-upgrade.sh` or `post-upgrade.sh` exists, Coral performs the file
+swap silently. It does **not** fall back to running pre-remove + post-install,
+as those hooks may have side effects inappropriate for an upgrade (e.g.,
+post-install creating a fresh database vs. preserving an existing one).
+
+### Stored scripts
+
+Remove scripts (`pre-remove.sh`, `post-remove.sh`) must be available when a
+package is removed, even if the original `.pkg.tar.xz` is gone. Coral stores
+a copy of `.SCRIPTS/` in the package database at install time
+(`/var/lib/coral/installed/<name>/scripts/`). Upgrade scripts are stored there
+as well, since they're needed when a future version replaces the current one.
+
+---
+
+## 6. Config File Protection — Checksum-Based
+
+### Current behavior (v1, broken)
+
+Reads both files fully into memory and compares content with `str.equals()`.
+Both branches of the comparison (equal / not equal) execute the same code path —
+always installs as `.pkg`. The "files are identical" case should overwrite in
+place.
+
+### New behavior (v2)
+
+Uses the sha256 checksum from the manifest to detect user modifications without
+reading the full installed file into memory twice.
+
+**On fresh install:**
+1. Install config file normally to `dest_path`
+2. Store the package-provided sha256 in the database for this file
+
+**On upgrade (file already exists):**
+1. Compute sha256 of the currently installed file
+2. Compare against the stored checksum from the *previously installed* version
+3. Decision:
+
+| Installed matches old pkg checksum? | Action |
+|-------------------------------------|--------|
+| Yes (user has not modified) | Overwrite with new version |
+| No (user has modified) | Install new version as `dest_path.pkg-new`, keep user's file |
+
+4. Update stored checksum to the new package's sha256
+
+**On remove:**
+- Config files listed in `.CONFIG` are **not** removed if they differ from the
+  package-provided checksum (user has modified them)
+- Config files that match the package checksum are removed normally
+
+This matches the proven dpkg conffile model.
+
+### Implementation
+
+```
+fn install_config_file(src: string, dest: string, manifest_sha256: string, stored_sha256: string): bool
+    if not fs.stat.exists(dest)
+        // Fresh install — copy file
+        fs.ops.copy_file_preserve(src, dest)
+        return true
+    end if
+
+    // File exists — check if user modified it
+    let installed_sha256 = checksum.sha256_file(dest)
+
+    if installed_sha256 == stored_sha256
+        // User has not modified — safe to overwrite
+        fs.ops.copy_file_preserve(src, dest)
+        return true
+    else
+        // User has modified — preserve their version
+        let new_path = dest + ".pkg-new"
+        fs.ops.copy_file_preserve(src, new_path)
+        // Log: "Config file preserved, new version at dest.pkg-new"
+        return true
+    end if
+end install_config_file
+```
+
+---
+
+## 7. Implementation Plan
+
+### Phase 1: mtree module and manifest generation
+- New module `util/mtree.reef` — parser and generator
+- Update `build.reef`: replace `generate_footprint()` with `generate_manifest()`
+- Use `fs.stat`, `fs.perm`, `fs.link.readlink()`, `checksum.sha256_file()`
+
+### Phase 2: fs.ops-based installer
+- Rewrite `install_files()` in `package.reef` to walk the manifest
+- Use `fs.ops.copy_file_preserve`, `fs.perm.chmod`, `fs.perm.chown`, `fs.link.symlink`
+- Remove rsync dependency from install path
+- Integrate config file checksum logic
+
+### Phase 3: Enhanced .PKGINFO
+- Update `generate_pkginfo()` to emit `[dependencies]` section
+- Update `read_pkginfo()` to parse dependencies
+- Add `deps` field to `PackageInfo` type or extend existing `Dependencies`
+
+### Phase 4: Script model cleanup
+- Add `pre-upgrade.sh` / `post-upgrade.sh` hook support
+- Remove `.reef` script type from `run_script()` and `run_stored_script()`
+- Change `.sh` invocation from `sh` to `zsh`
+- Pass version arguments to all hooks
+- Abort on non-zero exit from pre-* hooks
+- Update upgrade command to call upgrade hooks
+
+### Phase 5: Removal and verify
+- Rewrite `remove_files()` to use manifest (directories tracked explicitly)
+- Use `fs.ops.remove_file`, `fs.ops.remove_tree`, `dir.remove_dir`
+- Implement `coral verify <pkg>` — walk stored manifest, check sha256 and
+  permissions against live filesystem
+
+### Dependencies
+
+- Reef >= 0.5.0 (for `fs.stat`, `fs.perm`, `fs.link`, `fs.ops`)
+- rsync is no longer required for package installation (remains used by
+  `coral repo mirror` only)
+
+---
+
+## 8. Risks and Mitigations
+
+| Risk | Mitigation |
+|------|------------|
+| fs.ops.copy_file_preserve slower than rsync for large packages | Benchmark during Phase 2; rsync does buffered I/O and copy_file_preserve uses 8KB buffer — comparable. Manifest-driven install skips metadata files without needing exclude flags. |
+| mtree parser bugs cause install failures | Comprehensive test coverage in Phase 1; mtree is a simple line-oriented format with no nesting or escaping edge cases beyond spaces in paths. |
+| Packages with >8192 files overflow entry array | Use manifest file_count pre-scan or dynamic sizing. Current .FOOTPRINT has same limit. |
+| Upgrade hook ordering with dependency chains | Out of scope for this design. Coral currently upgrades packages individually. Dependency-ordered upgrades (topological sort) are a future enhancement. |
diff -r 000000000000 -r d0a96d73e9e1 docs/PACKAGE_FORMAT_V2_TODO.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/PACKAGE_FORMAT_V2_TODO.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,115 @@
+# Package Format v2 — Task List
+
+**Design:** `docs/PACKAGE_FORMAT_V2_DESIGN.md`
+**Target:** Coral v0.3
+**Date:** 2026-03-07
+**Requires:** Reef >= 0.5.0
+
+---
+
+## Phase 1: mtree module and manifest generation — COMPLETE
+
+- [x] Create `src/util/mtree.reef` module with `ManifestEntry` type
+- [x] Implement `parse_manifest()` — line-oriented parser, split on space, extract key=value pairs
+- [x] Implement `generate_manifest()` — walk directory tree using `fs.stat`, `fs.perm`, `fs.link.readlink()`, `checksum.sha256_file()`
+- [x] Implement accessor functions: `entry_type`, `entry_path`, `entry_mode`, `entry_sha256`, `entry_link`, `entry_uname`, `entry_gname`
+- [x] Handle sort order: lexicographic by path so dirs precede their contents
+- [x] Skip metadata files (`.PKGINFO`, `.MANIFEST`, `.CONFIG`, `.SCRIPTS/`) during generation
+- [x] Update `build.reef`: replace `generate_footprint()` with `generate_manifest()` using new mtree module
+- [x] Rename output file from `.FOOTPRINT` to `.MANIFEST` in build output
+- [ ] Test: build a package, verify `.MANIFEST` output contains correct types, modes, ownership, checksums
+- [ ] Test: parse a generated manifest back and verify round-trip correctness
+
+## Phase 2: fs.ops-based installer — COMPLETE
+
+- [x] Rewrite `install_files()` in `package.reef` to parse `.MANIFEST` instead of `.FOOTPRINT`
+- [x] Implement dir install: `dir.create_dir_all()`, `fs.perm.chmod()`, `fs.perm.chown()`
+- [x] Implement file install: `fs.ops.copy_file_preserve()`, `fs.perm.chmod()`, `fs.perm.chown()`
+- [x] Implement symlink install: remove conflicting target, `fs.link.symlink()`
+- [x] Add post-copy sha256 verification against manifest checksum
+- [x] Integrate config file checksum logic (see Phase 2b below)
+- [x] Remove rsync call from `install_files()`
+- [x] Remove symlink conflict prep_cmd shell-out (handle inline during manifest walk)
+- [x] Error handling: stop on any failed file operation
+- [x] Update `read_footprint()` to read `.MANIFEST` first, fall back to `.FOOTPRINT`
+- [x] Update `register_from_extract_dir()` to store `manifest.mtree` in database
+- [x] Update `remove_files()` to use `fs.ops.remove_file` instead of shell-outs
+- [x] Add `remove_files_manifest()` for manifest-driven removal with explicit directory tracking
+- [x] Clean up stale rsync comments in `install.reef`
+- [ ] Test: install a package to a test root, verify all files/dirs/symlinks match manifest
+- [ ] Test: install package with setuid binary (mode=4755), verify mode preserved
+- [ ] Test: install package with symlinks, verify targets correct
+
+### Phase 2b: Config file checksum logic — COMPLETE
+
+- [x] Update `install_config_file()` to accept manifest sha256 and stored sha256 parameters
+- [x] On fresh install: copy file normally
+- [x] On upgrade (file exists, matches old checksum): overwrite with new version
+- [x] On upgrade (file exists, differs from old checksum): install as `.pkg-new`, preserve user's file
+- [x] Remove old content-comparison logic (`str.equals(src_content, dest_content)`)
+- [x] On remove: skip config files where installed sha256 differs from package sha256
+- [ ] Test: fresh install places config file and records checksum
+- [ ] Test: upgrade with unmodified config overwrites cleanly
+- [ ] Test: upgrade with user-modified config installs `.pkg-new` and preserves original
+
+## Phase 3: Enhanced .PKGINFO — COMPLETE
+
+- [x] Add `runtime_deps`, `build_deps`, `runtime_deps_count`, `build_deps_count` fields to `PackageInfo` type in `types.reef`
+- [x] Update `new_package_info()` factory to initialize new fields
+- [x] Update `generate_pkginfo()` in `build.reef` to emit `[dependencies]` section
+- [x] Write `runtime` array from `ctx.port.deps.runtime`
+- [x] Write `build` array from `ctx.port.deps.build`
+- [x] Skip `[dependencies]` section if both arrays are empty
+- [x] Update `read_pkginfo()` in `package.reef` to parse `[dependencies]` section
+- [x] Handle legacy packages without `[dependencies]` gracefully
+- [x] Add `parse_toml_array()` helper to `package.reef`
+- [x] Handle version constraint syntax in dependency strings (`>=`, `<=`, `>`, `<`, `=`)
+- [x] Add version comparison utility for constraint checking
+- [ ] Test: build a package with dependencies, verify `.PKGINFO` contains correct `[dependencies]`
+- [ ] Test: parse `.PKGINFO` with dependencies and verify fields populated
+
+## Phase 4: Script model cleanup — COMPLETE
+
+- [x] Remove `.reef` script type from `run_script()` in `package.reef`
+- [x] Remove `.reef` script type from `run_stored_script()` in `package.reef`
+- [x] Change `.sh` invocation from `sh` to `zsh` in `run_script()`
+- [x] Change `.sh` invocation from `sh` to `zsh` in `run_stored_script()`
+- [x] Update script search order: try `.sh` first, then bare executable
+- [x] Add `pre-upgrade.sh` / `post-upgrade.sh` hook support
+- [x] Pass `$VERSION` argument to pre-install, post-install, pre-remove, post-remove
+- [x] Pass `$OLD_VERSION $NEW_VERSION` arguments to pre-upgrade, post-upgrade
+- [x] Abort install/remove/upgrade if pre-* hook exits non-zero
+- [x] Log warning (don't abort) if post-* hook exits non-zero
+- [x] Store upgrade scripts in database alongside remove scripts at install time
+- [x] Implement upgrade flow: pre-upgrade → remove old files → install new files → post-upgrade
+- [ ] Test: package with pre-install that exits 0 — install proceeds
+- [ ] Test: package with pre-install that exits 1 — install aborts
+- [ ] Test: package with post-install that exits 1 — install completes with warning
+- [ ] Test: upgrade with pre-upgrade/post-upgrade scripts, verify version args passed
+
+## Phase 5: Removal and verify — COMPLETE
+
+- [x] Rewrite `remove_files()` to use `fs.ops` instead of shell-outs (done in Phase 2)
+- [x] Add `remove_files_manifest()` with explicit directory tracking (done in Phase 2)
+- [x] Respect config file protection on remove (skip user-modified configs) (done in Phase 2b)
+- [x] Implement `coral verify <pkg>` command
+  - [x] Read stored manifest from database
+  - [x] For each file entry: check exists, compare sha256, compare mode/ownership
+  - [x] For each symlink entry: check exists, verify link target
+  - [x] For each dir entry: check exists
+  - [x] Report: missing files, checksum mismatches, permission changes, broken symlinks
+- [ ] Test: remove a package, verify all files deleted and empty dirs cleaned up
+- [ ] Test: remove a package with user-modified config, verify config preserved
+- [ ] Test: verify a clean install — all files pass
+- [ ] Test: verify after modifying a file — checksum mismatch reported
+
+---
+
+## Cleanup
+
+- [x] Remove `generate_footprint()` from `build.reef` (replaced by `generate_manifest()`)
+- [x] Update `read_footprint()` to read `.MANIFEST` via mtree parser (with `.FOOTPRINT` fallback)
+- [x] Update `verify_package()` to check for `.MANIFEST` or `.FOOTPRINT`
+- [x] Remove rsync from install path (only remains in `repo.reef` mirror command)
+- [x] Clean up rsync comments in `install.reef`
+- [x] Update `docs/GUIDE.md` — fix .reef references, sh→zsh, .pkg→.pkg-new, add verify command, add version constraints, add upgrade scripts
diff -r 000000000000 -r d0a96d73e9e1 docs/REFACTOR_SOURCES.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/REFACTOR_SOURCES.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,215 @@
+# Refactor: Source Mirrors Support
+
+**Status:** Planning
+**Version:** 0.1.3
+**Date:** January 2026
+
+---
+
+## Overview
+
+Refactor the source download system to support multiple mirror URLs per source file, with automatic fallback when primary sources are unavailable.
+
+---
+
+## Current Design
+
+```toml
+[sources]
+urls = ["https://example.com/file.tar.gz"]
+checksums = ["sha256:abc123..."]
+```
+
+**Problems:**
+- Multiple URLs treated as separate files, not mirrors
+- No fallback on download failure
+- Checksum tied to array index, fragile
+
+---
+
+## New Design
+
+### Package.toml Format
+
+```toml
+[[source]]
+file = "vim-9.1.0.tar.gz"
+urls = [
+    "https://github.com/vim/vim/archive/v9.1.0.tar.gz",
+    "https://mirror.example.com/vim-9.1.0.tar.gz",
+    "https://backup.example.org/vim-9.1.0.tar.gz"
+]
+checksum = "sha256:abc123..."
+
+[[source]]
+file = "illumos-compat.patch"
+urls = ["https://patches.zygaena.org/vim/illumos.patch"]
+checksum = "sha256:def456..."
+extract = false
+```
+
+### New Fields
+
+| Field | Required | Default | Description |
+|-------|----------|---------|-------------|
+| `file` | Yes | - | Output filename (what to save as) |
+| `urls` | Yes | - | Array of mirror URLs (tried in order) |
+| `checksum` | No | "SKIP" | SHA256 checksum for verification |
+| `extract` | No | true | Whether to extract archive |
+
+---
+
+## Implementation Plan
+
+### Phase 1: Type Definitions (types.reef)
+
+**New struct:**
+```reef
+type Source = struct
+    file: string        // Output filename
+    urls: [string]      // Mirror URLs (tried in order)
+    url_count: int      // Number of URLs
+    checksum: string    // SHA256 checksum
+    extract: bool       // Whether to extract (default: true)
+end Source
+```
+
+**Update SourceInfo:**
+```reef
+type SourceInfo = struct
+    sources: [Source]   // Array of Source structs
+    count: int          // Number of sources
+end SourceInfo
+```
+
+**Add factory function:**
+```reef
+fn new_source(): Source
+```
+
+### Phase 2: TOML Parsing (port.reef)
+
+Add support for parsing `[[source]]` table arrays:
+
+1. Detect `source.` prefixed keys
+2. Group by index (source.0.file, source.0.urls, etc.)
+3. Parse each source into Source struct
+4. Handle both old format (backward compat) and new format
+
+**Backward Compatibility:**
+- If `sources.urls` exists (old format), convert to new format
+- Each URL becomes a separate Source with auto-detected filename
+- Checksums mapped by index
+
+### Phase 3: Download Logic (build.reef)
+
+Update `download_sources()`:
+
+```
+for each source in sources:
+    if file exists in cache and checksum matches:
+        skip download
+
+    for each url in source.urls:
+        try download to source.file
+        if success:
+            verify checksum
+            if checksum ok:
+                break to next source
+            else:
+                delete file, try next mirror
+        else:
+            try next mirror
+
+    if all mirrors failed:
+        return error
+```
+
+### Phase 4: Extract Logic (build.reef)
+
+Update `extract_sources()`:
+
+```
+for each source in sources:
+    if source.extract:
+        extract archive
+    else:
+        copy file to work_dir (for patches, etc.)
+```
+
+### Phase 5: Update Test Ports
+
+Convert test ports to new format:
+- test/ports/base/vim/package.toml
+- test/ports/base/tree/package.toml
+- test/ports/core/coral/package.toml
+- etc.
+
+---
+
+## File Changes Summary
+
+| File | Changes |
+|------|---------|
+| `src/types.reef` | Add Source struct, update SourceInfo, add new_source() |
+| `src/core/port.reef` | Parse [[source]] tables, backward compat for old format |
+| `src/commands/build.reef` | Mirror fallback in download, respect extract flag |
+| `src/util/http.reef` | Add download_with_mirrors() helper (optional) |
+| `test/ports/*/package.toml` | Update to new format |
+
+---
+
+## Example Migration
+
+### Before (old format)
+```toml
+[sources]
+urls = ["https://github.com/vim/vim/archive/v9.1.0.tar.gz"]
+checksums = ["SKIP"]
+```
+
+### After (new format)
+```toml
+[[source]]
+file = "vim-9.1.0.tar.gz"
+urls = [
+    "https://github.com/vim/vim/archive/v9.1.0.tar.gz"
+]
+checksum = "SKIP"
+```
+
+### With mirrors
+```toml
+[[source]]
+file = "vim-9.1.0.tar.gz"
+urls = [
+    "https://github.com/vim/vim/archive/v9.1.0.tar.gz",
+    "https://ftp.nluug.nl/pub/vim/unix/vim-9.1.tar.bz2",
+    "https://mirror.freedif.org/vim/unix/vim-9.1.tar.bz2"
+]
+checksum = "sha256:1234567890abcdef..."
+```
+
+---
+
+## Testing Checklist
+
+- [ ] Single source, single URL (basic case)
+- [ ] Single source, multiple URLs (mirror fallback)
+- [ ] Multiple sources
+- [ ] Primary URL fails, mirror succeeds
+- [ ] All mirrors fail (error handling)
+- [ ] Checksum verification after download
+- [ ] Checksum mismatch triggers next mirror
+- [ ] `extract = false` for non-archive files
+- [ ] Backward compatibility with old format
+- [ ] Cached file with valid checksum skips download
+
+---
+
+## Future Enhancements
+
+- **Priority/weight** for mirrors (prefer faster/closer)
+- **Parallel downloads** from multiple mirrors
+- **Resume partial downloads**
+- **Mirror health tracking** (remember failed mirrors)
diff -r 000000000000 -r d0a96d73e9e1 docs/ROADMAP.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/ROADMAP.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,59 @@
+# Coral Roadmap
+
+**Target:** Coral v0.4.x
+**Date:** 2026-03-08
+**Tracks:** Unwired features, cleanup, and new functionality
+
+---
+
+## Phase 1: Cleanup
+
+- [x] **1. Remove redundant `opts.no_color` field**
+  - Removed `GlobalOptions.no_color` from `types.reef` and `main.reef`; color module reads flag directly
+- [x] **2. Remove legacy `SourceInfo.urls` / `SourceInfo.checksums` fields**
+  - Removed unused backward-compat fields from `types.reef` and dead writes from `port.reef`
+- [x] **3. Clean up resolver exports**
+  - Removed `resolve_alternative()`, `is_alternative_dep()`, `parse_alternatives()` from export block
+
+---
+
+## Phase 2: High Priority — Features users expect to work
+
+- [x] **4. Implement `--dry-run` / `-n` flag**
+  - Added dry-run support to install, remove, build, and upgrade commands
+- [x] **5. Wire up `fallback_to_source` config option**
+  - Install falls back to `coral build -y` when no binary package found and `fallback_to_source = true`
+- [x] **6. Implement package conflict detection**
+  - Install checks `conflicts` field against installed packages; `--force` overrides
+- [x] **7. Implement signature verification on install**
+  - When `require_signed_packages = true`, install verifies `.sig` file before proceeding
+- [x] **8. Implement `compress_man` build step**
+  - After build, gzips uncompressed man pages in staging dir when `compress_man = true`
+
+---
+
+## Phase 3: Medium Priority — Partially wired features
+
+- [x] **9. Wire up `keep_packages` / `keep_sources` config**
+  - `clean` command respects `keep_packages`/`keep_sources`; `--force` overrides protection
+- [x] **10. Wire up `use_proxy` config**
+  - HTTP module accepts proxy config; curl disables proxy by default, enables when `use_proxy = true`
+- [x] **11. Wire up `auto_refresh` config**
+  - Install and upgrade rebuild port index before execution when `auto_refresh = true`
+- [x] **12. Fix `coral diff` documentation**
+  - Updated GUIDE.md to reference `coral outdated` instead of `coral diff`
+- [x] **13. Use specific exit codes throughout codebase**
+  - Replaced generic `EXIT_ERROR()` with specific codes across all commands
+
+---
+
+## Completed
+
+- [x] Package Format v2 (mtree manifest, fs.ops installer, config protection) — v0.3.0
+- [x] Script model rewrite (zsh, upgrade hooks, version args) — v0.3.0
+- [x] Verify command — v0.3.0
+- [x] CI update to Reef 0.5.0 — v0.3.0
+- [x] Auto-install build dependencies — v0.3.1
+- [x] Export $PREFIX, $SYSCONFDIR, $PKG_ROOT to build scripts — v0.3.2
+- [x] Recursive build-from-source dependency resolver — v0.3.3
+- [x] All 13 unwired features (cleanup, high-priority, medium-priority) — v0.3.5
diff -r 000000000000 -r d0a96d73e9e1 docs/STAGE1_CORAL_PLAN.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/STAGE1_CORAL_PLAN.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,1893 @@
+# Stage 1: Coral Package Manager Implementation Plan
+
+**Document Version:** 2.5
+**Created:** January 2026
+**Last Updated:** January 2026
+**Status:** In Progress (Stub Implementation Complete)
+
+## Overview
+
+Stage 1 builds **Coral**, the package manager for Zygaena, written entirely in Reef. Coral is a single unified command with subcommands (similar to pacman/cargo).
+
+**Prerequisites:** Stage 0 Complete (Reef 0.1.3 installed on OmniOS at 192.168.168.148)
+
+---
+
+## Design Decisions
+
+| Decision | Choice | Rationale |
+|----------|--------|-----------|
+| **Name** | `coral` (ocean/reef theme) | Consistent with Zygaena (shark) and Reef (language) naming |
+| **Architecture** | Single binary with subcommands | Simpler for users, like pacman/cargo/git |
+| **Source Location** | `/home/ctusa/repos/zygaena/coral/` | Part of main zygaena repo for coordinated development |
+| **Install Location** | `/usr/local/bin/coral` (OmniOS), `/usr/bin/coral` (Zygaena) | Standard Unix paths |
+| **Compression** | xz (`.pkg.tar.xz`) | Good compression, widely available on illumos |
+| **HTTP** | Native Reef `net.http` (report issues to Reef team) | Avoid shell exec where possible |
+| **Privileges** | Root required for all operations | Multi-user system security |
+| **Confirmation** | Ask by default, `-y` to skip | Safe default, scriptable override |
+| **Colors** | Yes, when terminal supports it | Better UX, respects NO_COLOR standard |
+| **Port Categories** | core, base, devel, desktop, extra | Clear separation of concerns |
+| **Distribution** | Binary + Source packages | Binary for speed, source as fallback |
+| **Repository Access** | Local filesystem + HTTP | Both equally supported (ISO/USB and mirrors) |
+| **Package Signing** | Ed25519/Signify | Simple, minimal dependencies, proven security |
+| **Config Format** | TOML | Human-readable, matches port definitions |
+| **Third-party Repos** | Supported from day one | Enables community packages |
+
+---
+
+## Command Structure
+
+```
+coral <subcommand> [options] [packages...]
+
+BUILD/CREATE:
+  coral build <port>           Build package from port directory
+  coral build .                Build from current directory
+
+INSTALL/REMOVE:
+  coral install <pkg...>       Install packages (resolves dependencies)
+  coral remove <pkg...>        Remove packages
+  coral upgrade [pkg...]       Upgrade packages (all if none specified)
+
+QUERY:
+  coral info <pkg>             Show package details
+  coral list                   List installed packages
+  coral list -a                List all available packages (from repos)
+  coral files <pkg>            List files owned by package
+  coral owner <file>           Find which package owns file
+  coral search <term>          Search available packages
+  coral depends <pkg>          Show dependency tree
+
+PORTS:
+  coral sync                   Update ports collection and repo metadata (git/hg)
+  coral diff                   Show packages with available updates
+
+REPOSITORY MANAGEMENT:
+  coral repo init <path>       Create new repository structure
+  coral repo add <path> <pkg>  Add packages to repository
+  coral repo remove <path> <pkg> Remove package from repository
+  coral repo list <path>       List packages in repository
+  coral repo rebuild <path>    Regenerate repo.toml manifest
+  coral repo sign <path>       Sign repository manifest
+  coral repo verify <path>     Verify repository signatures
+  coral repo sync <path> <dest> Rsync repository to mirror
+  coral repo export <path> <out> Export repository for ISO/USB
+
+KEY MANAGEMENT:
+  coral key init               Initialize keyring directory
+  coral key generate <name>    Generate new Ed25519 signing keypair
+  coral key list               List available keys
+  coral key import <file>      Import public key
+  coral key export <name>      Export public key
+  coral key trust <keyid>      Mark key as trusted
+  coral key revoke <keyid>     Revoke trust for key
+
+CACHE MANAGEMENT:
+  coral cache clean            Remove cached packages
+  coral cache clean --sources  Remove cached source tarballs
+  coral cache clean --all      Remove all cached files
+  coral cache list             Show cache contents and sizes
+
+GLOBAL OPTIONS:
+  -y, --yes                    Don't ask for confirmation
+  -f, --force                  Force operation (overwrite conflicts)
+  -v, --verbose                Verbose output
+  -n, --dry-run                Show what would be done
+  -q, --quiet                  Minimal output
+  --no-color                   Disable colored output
+  --root <path>                Use alternative root (for building ISOs)
+
+EXAMPLES:
+  coral install vim            # Install vim and dependencies
+  coral install vim nano -y    # Install multiple, no confirmation
+  coral build /usr/ports/core/hello
+  coral search editor
+  coral upgrade                # Upgrade all packages
+  coral remove firefox -y
+  coral repo init /srv/packages
+  coral key generate official
+```
+
+---
+
+## Configuration
+
+### Main Configuration File
+
+Location: `/etc/coral/coral.conf`
+
+```toml
+# Coral Package Manager Configuration
+
+[general]
+# System architecture
+arch = "x86_64"
+
+# Number of parallel build jobs (0 = auto-detect CPU count)
+jobs = 0
+
+# Ask for confirmation before operations (override with -y)
+confirm = true
+
+# Enable colored output (respects NO_COLOR env var)
+color = true
+
+[paths]
+# Local ports collection
+ports = "/usr/ports"
+
+# Built package cache
+packages = "/var/lib/coral/packages"
+
+# Downloaded source tarballs
+sources = "/var/lib/coral/sources"
+
+# Installed package database
+database = "/var/lib/coral/installed"
+
+# Repository metadata cache
+repos = "/var/lib/coral/repos"
+
+# Build working directory (cleaned after build)
+work = "/var/tmp/coral/work"
+
+# Package staging directory
+pkg = "/var/tmp/coral/pkg"
+
+# Signing keys directory
+keys = "/etc/coral/keys"
+
+[build]
+# Default CFLAGS for building packages
+cflags = "-O2 -pipe -march=x86-64"
+
+# Default CXXFLAGS (defaults to cflags if empty)
+cxxflags = "${cflags}"
+
+# Make flags
+makeflags = "-j${jobs}"
+
+# Strip binaries after build
+strip = true
+
+# Compress man pages with gzip
+compress_man = true
+
+# What to do when binary package not available:
+# "ask" - prompt user to build from source
+# "yes" - automatically build from source
+# "no"  - fail if binary not available
+fallback_to_source = "ask"
+
+[network]
+# Connection timeout in seconds
+timeout = 30
+
+# Number of retry attempts
+retries = 3
+
+# Use system proxy settings
+use_proxy = true
+
+[sync]
+# Auto-refresh repository metadata (hours, 0 = manual only)
+auto_refresh = 24
+
+[cache]
+# Keep downloaded packages after installation
+keep_packages = true
+
+# Keep downloaded source tarballs
+keep_sources = true
+
+[security]
+# Require repositories to have valid signatures
+require_signed_repos = true
+
+# Require packages to have valid signatures
+require_signed_packages = true
+```
+
+### Repository Configuration Directory
+
+Location: `/etc/coral/repos.d/`
+
+Each repository is configured in a separate TOML file.
+
+#### Official Repository Example
+
+File: `/etc/coral/repos.d/official.toml`
+
+```toml
+[repository]
+# Unique identifier
+name = "official"
+
+# Human-readable description
+description = "Official Zygaena packages"
+
+# Enable/disable this repository
+enabled = true
+
+# Priority (higher = preferred when same package in multiple repos)
+priority = 100
+
+# Mirrors to try (in order)
+[[mirrors]]
+url = "https://pkg.zygaena.org/stable/${arch}/"
+
+[[mirrors]]
+url = "https://mirror.example.com/zygaena/${arch}/"
+
+# Local fallback (e.g., installation media)
+[[mirrors]]
+url = "file:///mnt/cdrom/packages/"
+
+[security]
+# Require signed packages from this repo
+require_signed = true
+
+# Public key file for signature verification
+keyfile = "/etc/coral/keys/official.pub"
+```
+
+#### Third-Party Repository Example
+
+File: `/etc/coral/repos.d/community.toml`
+
+```toml
+[repository]
+name = "community"
+description = "Community-maintained packages"
+enabled = true
+priority = 50
+
+[[mirrors]]
+url = "https://community.example.org/zygaena/${arch}/"
+
+[security]
+require_signed = true
+keyfile = "/etc/coral/keys/community.pub"
+```
+
+---
+
+## Repository Manifest Format
+
+Each repository publishes a `repo.toml` manifest that coral downloads to understand available packages.
+
+### Manifest Structure
+
+File: `repo.toml` (at repository root)
+
+```toml
+[repository]
+# Repository identifier (must match repos.d config)
+name = "official"
+
+# Human-readable description
+description = "Official Zygaena packages"
+
+# Maintainer contact
+maintainer = "[email protected]"
+
+# Target architecture
+arch = "x86_64"
+
+# When this manifest was generated (ISO 8601)
+generated = 2026-01-23T12:00:00Z
+
+# Total number of packages
+package_count = 150
+
+# Base URL for package downloads (relative to manifest location)
+base_url = "packages/"
+
+# Signing key identifier
+signing_key = "zygaena-official-2026"
+
+# Package entries
+[[package]]
+name = "bash"
+version = "5.2.21"
+release = 1
+description = "The GNU Bourne Again shell"
+license = "GPL-3.0"
+category = "core"
+depends = ["glibc", "readline", "ncurses"]
+optdepends = []
+provides = ["sh"]
+conflicts = []
+size = 7834521           # Installed size in bytes
+compressed = 1823456     # Package file size in bytes
+filename = "bash-5.2.21-1.pkg.tar.xz"
+sha256 = "a1b2c3d4e5f6789..."
+signature = "sig:ed25519:..."  # Optional inline signature
+
+[[package]]
+name = "vim"
+version = "9.1.0"
+release = 1
+description = "Vi IMproved - enhanced vi editor"
+license = "Vim"
+category = "base"
+depends = ["ncurses"]
+optdepends = ["python:Python scripting support", "lua:Lua scripting support"]
+provides = ["vi"]
+conflicts = []
+size = 12456789
+compressed = 3456789
+filename = "vim-9.1.0-1.pkg.tar.xz"
+sha256 = "fedcba987654321..."
+signature = "sig:ed25519:..."
+
+# ... more packages ...
+```
+
+### Manifest Signature
+
+The manifest itself is signed. Signature stored in: `repo.toml.sig`
+
+```
+-----BEGIN ED25519 SIGNATURE-----
+<base64 encoded signature>
+-----END ED25519 SIGNATURE-----
+```
+
+---
+
+## Package Signing
+
+### Key Format
+
+Coral uses Ed25519 keys (same as OpenBSD signify).
+
+**Private key:** `<name>.sec`
+```
+untrusted comment: coral secret key
+<base64 encoded private key>
+```
+
+**Public key:** `<name>.pub`
+```
+untrusted comment: coral public key
+<base64 encoded public key>
+```
+
+### Key Storage
+
+- System keys: `/etc/coral/keys/`
+- User keys: `~/.coral/keys/`
+- Trusted keys database: `/var/lib/coral/trusted-keys`
+
+### Signing Workflow
+
+```bash
+# Generate a keypair for repository signing
+coral key generate official
+
+# Sign the repository manifest
+coral repo sign /srv/packages
+
+# Verify a repository
+coral repo verify /srv/packages
+
+# Import a third-party public key
+coral key import community.pub
+
+# Mark the key as trusted
+coral key trust community
+```
+
+---
+
+## Source Code Structure
+
+```
+/home/ctusa/repos/zygaena/
+├── coral/                        # Package manager source
+│   ├── reef.toml                 # Reef project config
+│   └── src/
+│       ├── main.reef             # Entry point, CLI dispatch
+│       ├── commands/
+│       │   ├── build.reef        # coral build
+│       │   ├── install.reef      # coral install
+│       │   ├── remove.reef       # coral remove
+│       │   ├── upgrade.reef      # coral upgrade
+│       │   ├── info.reef         # coral info
+│       │   ├── list.reef         # coral list
+│       │   ├── files.reef        # coral files
+│       │   ├── owner.reef        # coral owner
+│       │   ├── search.reef       # coral search
+│       │   ├── depends.reef      # coral depends
+│       │   ├── sync.reef         # coral sync
+│       │   ├── diff.reef         # coral diff
+│       │   ├── repo.reef         # coral repo (subcommands)
+│       │   ├── key.reef          # coral key (subcommands)
+│       │   └── cache.reef        # coral cache (subcommands)
+│       ├── core/
+│       │   ├── port.reef         # Port parsing (package.toml)
+│       │   ├── package.reef      # Package archive handling
+│       │   ├── database.reef     # Installed package database
+│       │   ├── resolver.reef     # Dependency resolution
+│       │   ├── config.reef       # Configuration loading
+│       │   ├── repository.reef   # Repository manifest handling
+│       │   └── signing.reef      # Ed25519 signing/verification
+│       ├── util/
+│       │   ├── http.reef         # HTTP downloads (wraps net.http)
+│       │   ├── archive.reef      # Tar/xz operations
+│       │   ├── checksum.reef     # SHA256 verification
+│       │   ├── color.reef        # Terminal colors
+│       │   └── prompt.reef       # User prompts
+│       └── types.reef            # Shared type definitions
+├── ports/                        # Port collection (Stage 2)
+│   ├── core/
+│   ├── base/
+│   ├── devel/
+│   ├── desktop/
+│   └── extra/
+├── docs/
+│   ├── STAGE0_BOOTSTRAP.md
+│   ├── STAGE1_CORAL_PLAN.md      # This document
+│   └── ...
+└── bootstrap/
+```
+
+---
+
+## Port Categories
+
+| Category | Description | Examples |
+|----------|-------------|----------|
+| **core** | Essential system packages | kernel, libc, coreutils, bash |
+| **base** | Standard Unix utilities | vim, git, curl, openssh |
+| **devel** | Development tools & libraries | gcc, cmake, python, rust, ncurses-dev |
+| **desktop** | X11, window managers, GUI apps | xlibre, windowmaker, firefox |
+| **extra** | Everything else | games, themes, misc utilities |
+
+---
+
+## Reef Language Notes
+
+### Reserved Keywords
+**IMPORTANT:** `run` is a reserved keyword for Active Objects in Reef. Command handlers must use `execute()` instead of `run()`.
+
+### Key Syntax Rules
+- Blocks end with `end` keyword: `end if`, `end for`, `end while`, `end FunctionName`, `end module`
+- `fn` returns a value, `proc` does not return a value
+- String interpolation: `"Hello, ${name}"` (simple variables only, not expressions)
+- Mutable variables: `mut count = 0`
+- Immutable variables: `let value = 42`
+- Logical NOT: `not` (not `!`)
+- Array allocation: `new [type](size)`
+- Module declaration: `module path.to.module` ... `end module`
+- Export block: `export` ... `end export`
+- Entry point (`main.reef`) does NOT use a module declaration
+
+### Module Structure Pattern
+```reef
+module module.name
+
+export
+    // Public interface declarations
+    fn public_function(): type
+    proc public_procedure()
+end export
+
+// Implementation
+fn public_function(): type
+    // ...
+end public_function
+
+// Private helpers (not in export block)
+fn private_helper(): type
+    // ...
+end private_helper
+
+end module
+```
+
+### TOML Parsing Pattern
+The Reef stdlib uses parallel arrays for TOML parsing:
+```reef
+import encoding.toml
+
+let keys = toml.toml_alloc_keys()      // Allocate keys array (max 256)
+let vals = toml.toml_alloc_values()    // Allocate values array
+let count = toml.toml_parse(content, keys, vals)
+let name = toml.toml_get(keys, vals, count, "package.name")
+let release = toml.toml_get_int(keys, vals, count, "package.release")
+```
+
+---
+
+## Reef Standard Library Modules
+
+```reef
+// Core imports for Coral
+import io.file            // readFile, writeFile, fileExists, readBinaryFile
+import io.dir             // list_dir, dir_exists, create_dir_all, current_dir
+import io.path            // join_path, dirname, basename, extension
+import core.str           // split, join, contains, starts_with, ends_with, trim_ws
+import sys.args           // count, get, has_flag (simple CLI parsing)
+import sys.flag           // FlagParser for rich CLI parsing with help generation
+import sys.process        // process_spawn_shell, process_wait
+import sys.env            // get_env, set_env, has_env
+import io.console         // readLine, confirm, printErr
+import encoding.toml      // toml_parse, toml_get, toml_get_int, toml_has_key
+import net.http           // HTTP client (HTTPS support coming from Reef team)
+import time.clock         // Timestamps
+import collections.list   // Dynamic lists
+import collections.map    // Hash maps
+import crypto.sha256      // SHA256 checksums (native, pure Reef implementation)
+```
+
+### Native vs Shell-out
+
+| Functionality | Implementation | Notes |
+|---------------|----------------|-------|
+| **SHA256** | Native `crypto.sha256` | Pure Reef, supports string and binary data |
+| **TOML parsing** | Native `encoding.toml` | Uses parallel arrays pattern |
+| **HTTP** | Native `net.http` | HTTP now, HTTPS coming from Reef team |
+| **File I/O** | Native `io.file`, `io.dir` | Full support |
+| **Tar/xz** | Shell-out to `tar`, `xz` | System tools, acceptable |
+| **Patch** | Shell-out to `patch` | Apply source patches (-p<strip>) |
+| **Ed25519 signing** | Shell-out to `signify` | Until Reef adds crypto.ed25519 |
+| **Git** | Shell-out to `git` | For `coral sync` ports update |
+| **Mercurial** | Shell-out to `hg` | Alternative VCS for ports update |
+
+---
+
+## Core Data Types
+
+```reef
+// types.reef - Shared type definitions
+
+// Package metadata from package.toml
+type PackageInfo = struct
+    name: string
+    version: string
+    release: int
+    description: string
+    url: string
+    license: string
+    maintainer: string
+    arch: string
+    category: string
+    alternatives: [string]  // Alternative sets this package belongs to
+    alternatives_count: int
+    conflicts: [string]     // Packages that conflict with this
+    conflicts_count: int
+end PackageInfo
+
+type Dependencies = struct
+    runtime: [string]
+    build: [string]
+    optional: [string]      // Optional dependencies with descriptions
+    runtime_count: int
+    build_count: int
+    optional_count: int
+end Dependencies
+
+type SourceInfo = struct
+    urls: [string]
+    checksums: [string]
+    count: int
+end SourceInfo
+
+type PatchInfo = struct
+    files: [string]         // Patch filenames in order
+    strip: int              // Strip level for patch -p (default: 1)
+    count: int              // Number of patches
+end PatchInfo
+
+type ConfigInfo = struct
+    files: [string]         // Config files to protect (installed as .pkg)
+    count: int
+end ConfigInfo
+
+type ScriptsInfo = struct
+    pre_install: string     // Script filename or empty
+    post_install: string
+    pre_remove: string
+    post_remove: string
+end ScriptsInfo
+
+type BuildConfig = struct
+    parallel: bool
+    jobs: int
+end BuildConfig
+
+// Complete port definition
+type Port = struct
+    info: PackageInfo
+    deps: Dependencies
+    sources: SourceInfo
+    patches: PatchInfo
+    config: ConfigInfo
+    scripts: ScriptsInfo
+    build_cfg: BuildConfig
+    port_dir: string
+end Port
+
+// Installed package record
+type InstalledPackage = struct
+    info: PackageInfo
+    install_date: string
+    install_reason: string    // "explicit" or "dependency"
+    files: [string]           // All files owned by this package
+    files_count: int
+    config_files: [string]    // Config files (for upgrade handling)
+    config_count: int
+    repository: string        // Which repo it came from
+end InstalledPackage
+
+// Remote package from repository manifest
+type RemotePackage = struct
+    info: PackageInfo
+    depends: [string]
+    optdepends: [string]
+    provides: [string]
+    conflicts: [string]
+    size: int                 // Installed size
+    compressed: int           // Download size
+    filename: string
+    sha256: string
+    signature: string
+    depends_count: int
+end RemotePackage
+
+// Repository definition
+type Repository = struct
+    name: string
+    description: string
+    enabled: bool
+    priority: int
+    mirrors: [string]
+    mirror_count: int
+    keyfile: string
+    require_signed: bool
+end Repository
+
+// Build result
+type BuildResult = struct
+    success: bool
+    package_path: string
+    error_message: string
+end BuildResult
+
+// Signing key
+type SigningKey = struct
+    name: string
+    public_key: string
+    fingerprint: string
+    trusted: bool
+    created: string
+end SigningKey
+```
+
+---
+
+## Key Implementation Details
+
+### 1. Main Entry Point (main.reef)
+
+```reef
+// main.reef - Entry point (no module declaration)
+// Entry point files do NOT use module declarations in Reef
+
+import sys.args
+import commands.build
+import commands.install
+import commands.remove
+import commands.repo
+import commands.key
+import commands.cache
+// ... other imports
+
+proc main()
+    // Check for root (except for query commands)
+    if not is_query_command() and not is_root()
+        print_error("coral: must be run as root")
+        return
+    end if
+
+    if args.count() < 2
+        print_usage()
+        return
+    end if
+
+    let cmd = args.get(1)
+
+    // Parse global options
+    let opts = parse_global_options()
+
+    // Dispatch to subcommand
+    if cmd == "build"
+        build.execute(opts)
+    elif cmd == "install"
+        install.execute(opts)
+    elif cmd == "remove"
+        remove.execute(opts)
+    elif cmd == "upgrade"
+        upgrade.execute(opts)
+    elif cmd == "info"
+        info.execute(opts)
+    elif cmd == "list"
+        list.execute(opts)
+    elif cmd == "files"
+        files.execute(opts)
+    elif cmd == "owner"
+        owner.execute(opts)
+    elif cmd == "search"
+        search.execute(opts)
+    elif cmd == "depends"
+        depends.execute(opts)
+    elif cmd == "sync"
+        sync.execute(opts)
+    elif cmd == "diff"
+        diff.execute(opts)
+    elif cmd == "repo"
+        repo.execute(opts)
+    elif cmd == "key"
+        key.execute(opts)
+    elif cmd == "cache"
+        cache.execute(opts)
+    elif cmd == "--help" or cmd == "-h"
+        print_usage()
+    elif cmd == "--version"
+        println("coral 1.0.0")
+    else
+        print_error("coral: unknown command '${cmd}'")
+        print_usage()
+    end if
+end main
+
+fn is_query_command(): bool
+    let cmd = args.get(1)
+    return cmd == "info" or cmd == "list" or cmd == "files" or
+           cmd == "owner" or cmd == "search" or cmd == "depends" or
+           cmd == "diff" or cmd == "--help" or cmd == "-h" or
+           cmd == "--version" or cmd == "cache"
+end is_query_command
+
+fn is_root(): bool
+    // Check if running as root (uid 0)
+    let uid_str = env.get_env("EUID")
+    if str.length(uid_str) == 0
+        // Fallback: try id command
+        // For now assume root if EUID not set
+        return true
+    end if
+    return uid_str == "0"
+end is_root
+```
+
+### 2. Repository Management (commands/repo.reef)
+
+```reef
+module commands.repo
+
+import sys.args
+import io.file
+import io.dir
+import io.path
+import encoding.toml
+import core.signing
+import util.color
+
+proc execute(opts: GlobalOptions)
+    if args.count() < 3
+        print_repo_usage()
+        return
+    end if
+
+    let subcmd = args.get(2)
+
+    if subcmd == "init"
+        repo_init(opts)
+    elif subcmd == "add"
+        repo_add(opts)
+    elif subcmd == "remove"
+        repo_remove(opts)
+    elif subcmd == "list"
+        repo_list(opts)
+    elif subcmd == "rebuild"
+        repo_rebuild(opts)
+    elif subcmd == "sign"
+        repo_sign(opts)
+    elif subcmd == "verify"
+        repo_verify(opts)
+    elif subcmd == "sync"
+        repo_sync(opts)
+    elif subcmd == "export"
+        repo_export(opts)
+    else
+        print_error("Unknown repo subcommand: ${subcmd}")
+        print_repo_usage()
+    end if
+end run
+
+proc repo_init(opts: GlobalOptions)
+    if args.count() < 4
+        print_error("Usage: coral repo init <path>")
+        return
+    end if
+
+    let repo_path = args.get(3)
+
+    // Create directory structure
+    dir.create_dir_all(path.join_path(repo_path, "packages"))
+
+    // Create initial repo.toml
+    let manifest = """
+[repository]
+name = "unnamed"
+description = "New repository"
+maintainer = ""
+arch = "x86_64"
+generated = ""
+package_count = 0
+base_url = "packages/"
+signing_key = ""
+"""
+
+    file.writeFile(path.join_path(repo_path, "repo.toml"), manifest)
+    print_success("Repository initialized at ${repo_path}")
+end repo_init
+
+proc repo_rebuild(opts: GlobalOptions)
+    if args.count() < 4
+        print_error("Usage: coral repo rebuild <path>")
+        return
+    end if
+
+    let repo_path = args.get(3)
+    let pkg_dir = path.join_path(repo_path, "packages")
+
+    // Scan all .pkg.tar.xz files
+    let packages = dir.list_dir(pkg_dir)
+
+    // Build manifest from package metadata
+    // ... extract .PKGINFO from each package
+    // ... generate repo.toml
+
+    print_success("Repository manifest rebuilt")
+end repo_rebuild
+
+proc repo_sign(opts: GlobalOptions)
+    if args.count() < 4
+        print_error("Usage: coral repo sign <path> [--key <name>]")
+        return
+    end if
+
+    let repo_path = args.get(3)
+    let manifest_path = path.join_path(repo_path, "repo.toml")
+
+    let key_name = args.get_flag_value("key")
+    if str.length(key_name) == 0
+        key_name = "default"
+    end if
+
+    // Sign the manifest
+    let sig = signing.sign_file(manifest_path, key_name)
+    file.writeFile(manifest_path + ".sig", sig)
+
+    print_success("Repository signed with key '${key_name}'")
+end repo_sign
+```
+
+### 3. Key Management (commands/key.reef)
+
+```reef
+module commands.key
+
+import sys.args
+import io.file
+import io.dir
+import io.path
+import core.signing
+import util.color
+
+proc execute(opts: GlobalOptions)
+    if args.count() < 3
+        print_key_usage()
+        return
+    end if
+
+    let subcmd = args.get(2)
+
+    if subcmd == "init"
+        key_init(opts)
+    elif subcmd == "generate"
+        key_generate(opts)
+    elif subcmd == "list"
+        key_list(opts)
+    elif subcmd == "import"
+        key_import(opts)
+    elif subcmd == "export"
+        key_export(opts)
+    elif subcmd == "trust"
+        key_trust(opts)
+    elif subcmd == "revoke"
+        key_revoke(opts)
+    else
+        print_error("Unknown key subcommand: ${subcmd}")
+        print_key_usage()
+    end if
+end run
+
+proc key_generate(opts: GlobalOptions)
+    if args.count() < 4
+        print_error("Usage: coral key generate <name>")
+        return
+    end if
+
+    let name = args.get(3)
+    let keys_dir = "/etc/coral/keys"
+
+    // Generate Ed25519 keypair
+    let keypair = signing.generate_keypair()
+
+    // Save private key (restricted permissions)
+    let sec_path = path.join_path(keys_dir, name + ".sec")
+    file.writeFile(sec_path, keypair.private_key)
+    // TODO: chmod 600 on sec_path
+
+    // Save public key
+    let pub_path = path.join_path(keys_dir, name + ".pub")
+    file.writeFile(pub_path, keypair.public_key)
+
+    print_success("Generated keypair '${name}'")
+    println("  Private key: ${sec_path}")
+    println("  Public key:  ${pub_path}")
+end key_generate
+
+proc key_list(opts: GlobalOptions)
+    let keys_dir = "/etc/coral/keys"
+    let files = dir.list_dir(keys_dir)
+
+    println("Available signing keys:")
+    println("")
+
+    for f in files
+        if str.ends_with(f, ".pub")
+            let name = str.replace(f, ".pub", "")
+            let has_private = file.fileExists(path.join_path(keys_dir, name + ".sec"))
+            let key_type = "public"
+            if has_private
+                key_type = "public + private"
+            end if
+            println("  ${name} (${key_type})")
+        end if
+    end for
+end key_list
+```
+
+### 4. Terminal Colors (util/color.reef)
+
+```reef
+module util.color
+
+import sys.env
+import sys.args
+
+// ANSI color codes
+fn red(s: string): string
+    if no_color()
+        return s
+    end if
+    return "\x1b[31m" + s + "\x1b[0m"
+end red
+
+fn green(s: string): string
+    if no_color()
+        return s
+    end if
+    return "\x1b[32m" + s + "\x1b[0m"
+end green
+
+fn yellow(s: string): string
+    if no_color()
+        return s
+    end if
+    return "\x1b[33m" + s + "\x1b[0m"
+end yellow
+
+fn blue(s: string): string
+    if no_color()
+        return s
+    end if
+    return "\x1b[34m" + s + "\x1b[0m"
+end blue
+
+fn bold(s: string): string
+    if no_color()
+        return s
+    end if
+    return "\x1b[1m" + s + "\x1b[0m"
+end bold
+
+fn no_color(): bool
+    // Check --no-color flag or NO_COLOR env var
+    return args.has_flag("no-color") or env.has_env("NO_COLOR")
+end no_color
+
+// Formatted output helpers
+proc print_success(msg: string)
+    println(green("✓") + " " + msg)
+end print_success
+
+proc print_error(msg: string)
+    println(red("✗") + " " + msg)
+end print_error
+
+proc print_warning(msg: string)
+    println(yellow("!") + " " + msg)
+end print_warning
+
+proc print_info(msg: string)
+    println(blue("→") + " " + msg)
+end print_info
+```
+
+### 5. User Prompts (util/prompt.reef)
+
+```reef
+module util.prompt
+
+import sys.args
+import io.console
+import core.str
+
+fn confirm(message: string): bool
+    // Skip if -y flag
+    if args.has_flag("y") or args.has_flag("yes")
+        return true
+    end if
+
+    print(message + " [Y/n] ")
+    let response = console.readLine()
+    let r = str.trim_ws(response)
+
+    // Empty or Y/y means yes
+    return str.length(r) == 0 or r == "Y" or r == "y" or r == "yes"
+end confirm
+
+fn confirm_packages(action: string, packages: [string], count: int): bool
+    if count == 0
+        return false
+    end if
+
+    println("")
+    println("Packages to ${action}:")
+    for i in 0 to count
+        println("  " + packages[i])
+    end for
+    println("")
+
+    return confirm("Proceed?")
+end confirm_packages
+
+fn ask_build_from_source(pkg_name: string): bool
+    println("")
+    print_warning("Binary package not available for '${pkg_name}'")
+    return confirm("Build from source?")
+end ask_build_from_source
+```
+
+### 6. HTTP Downloads (util/http.reef)
+
+```reef
+module util.http
+
+import net.http
+import io.file
+import util.color
+import core.config
+
+// Download a file from URL to destination
+// Returns true on success
+fn download(url: string, dest: string): bool
+    print_info("Downloading: ${url}")
+
+    let cfg = config.load()
+
+    // Try native Reef HTTP first
+    let result = http.get(url, {
+        timeout: cfg.network.timeout,
+        retries: cfg.network.retries
+    })
+
+    if result.status_code == 200
+        file.writeFile(dest, result.body)
+        return true
+    elif result.status_code >= 300 and result.status_code < 400
+        // Handle redirect
+        let redirect_url = result.headers["Location"]
+        if str.length(redirect_url) > 0
+            return download(redirect_url, dest)
+        end if
+    end if
+
+    print_error("HTTP error: ${result.status_code}")
+    return false
+end download
+
+// Download with progress callback
+fn download_with_progress(url: string, dest: string, callback: fn(int, int)): bool
+    // Similar to download but calls callback(bytes_received, total_bytes)
+    // Implementation depends on net.http capabilities
+    return download(url, dest)  // Fallback for now
+end download_with_progress
+
+// Note: If net.http fails on illumos, we'll report to Reef team
+// and can add a fallback to curl if absolutely necessary
+```
+
+### 7. Build Command (commands/build.reef)
+
+```reef
+module commands.build
+
+import sys.args
+import io.file
+import io.dir
+import io.path
+import core.port
+import core.package
+import util.color
+import util.http
+import util.checksum
+
+proc execute(opts: GlobalOptions)
+    if args.count() < 3
+        print_error("Usage: coral build <port-directory>")
+        return
+    end if
+
+    let port_path = args.get(2)
+
+    // Handle "." for current directory
+    let actual_path = port_path
+    if port_path == "."
+        actual_path = dir.current_dir()
+    end if
+
+    // Load port
+    let port_result = port.load(actual_path)
+    if not port_result.success
+        print_error("Failed to load port: ${port_result.error}")
+        return
+    end if
+
+    let p = port_result.port
+
+    println(bold("Building ${p.info.name}-${p.info.version}"))
+    println("")
+
+    // Step 1: Download sources
+    print_info("Downloading sources...")
+    if not download_sources(p)
+        print_error("Failed to download sources")
+        return
+    end if
+
+    // Step 2: Verify checksums
+    print_info("Verifying checksums...")
+    if not verify_sources(p)
+        print_error("Checksum verification failed")
+        return
+    end if
+
+    // Step 3: Extract sources
+    print_info("Extracting sources...")
+    if not extract_sources(p)
+        print_error("Failed to extract sources")
+        return
+    end if
+
+    // Step 4: Apply patches (if any)
+    if p.patches.count > 0
+        print_info("Applying patches...")
+        if not apply_patches(p)
+            print_error("Failed to apply patches")
+            return
+        end if
+    end if
+
+    // Step 5: Run build
+    print_info("Building...")
+    if not run_build(p, opts.verbose)
+        print_error("Build failed")
+        return
+    end if
+
+    // Step 6: Create package
+    print_info("Creating package...")
+    let pkg_path = create_package(p)
+    if str.length(pkg_path) == 0
+        print_error("Failed to create package")
+        return
+    end if
+
+    println("")
+    print_success("Package created: ${pkg_path}")
+end run
+
+fn download_sources(p: Port): bool
+    for i in 0 to p.sources.count
+        let url = p.sources.urls[i]
+        let filename = path.basename(url)
+        let dest = path.join_path(SOURCES_DIR, filename)
+
+        if file.fileExists(dest)
+            println("  " + green("✓") + " ${filename} (cached)")
+            continue
+        end if
+
+        if not http.download(url, dest)
+            return false
+        end if
+    end for
+    return true
+end download_sources
+
+fn apply_patches(p: Port): bool
+    let work_dir = path.join_path(WORK_DIR, p.info.name)
+    let patches_dir = path.join_path(p.port_dir, "patches")
+    let strip = p.patches.strip
+
+    for i in 0 to p.patches.count
+        let patch_file = p.patches.files[i]
+        let patch_path = path.join_path(patches_dir, patch_file)
+
+        println("  Applying: ${patch_file}")
+
+        // patch -p<strip> -d <work_dir> < <patch_path>
+        let cmd = "patch -p" + to_string(strip) + " -d " + work_dir + " < " + patch_path
+        let pid = process.process_spawn_shell(cmd)
+        let exit_code = process.process_wait(pid)
+
+        if exit_code != 0
+            print_error("Patch failed: ${patch_file}")
+            return false
+        end if
+    end for
+    return true
+end apply_patches
+
+fn create_package(p: Port): string
+    let pkg_name = p.info.name + "-" + p.info.version + "-" +
+                   to_string(p.info.release) + ".pkg.tar.xz"
+    let pkg_path = path.join_path(PACKAGES_DIR, pkg_name)
+    let pkg_staging = path.join_path(PKG_DIR, p.info.name)
+
+    // Create .PKGINFO
+    package.create_pkginfo(p, pkg_staging)
+
+    // Create .FOOTPRINT
+    package.create_footprint(pkg_staging)
+
+    // Create tarball with xz compression
+    let cmd = "cd " + pkg_staging + " && tar -Jcf " + pkg_path + " ."
+    let pid = process.process_spawn_shell(cmd)
+    let exit_code = process.process_wait(pid)
+
+    if exit_code != 0
+        return ""
+    end if
+
+    return pkg_path
+end create_package
+```
+
+### 8. Install Command (commands/install.reef)
+
+```reef
+module commands.install
+
+import sys.args
+import core.resolver
+import core.database
+import core.port
+import core.repository
+import core.config
+import util.color
+import util.prompt
+
+proc execute(opts: GlobalOptions)
+    if args.count() < 3
+        print_error("Usage: coral install <package...>")
+        return
+    end if
+
+    // Collect package names from args
+    let packages: [string] = new [string](50)
+    mut pkg_count = 0
+    for i in 2 to args.count()
+        let arg = args.get(i)
+        if not str.starts_with(arg, "-")
+            packages[pkg_count] = arg
+            pkg_count = pkg_count + 1
+        end if
+    end for
+
+    // Resolve dependencies for all packages
+    let to_install: [string] = new [string](100)
+    mut install_count = 0
+
+    for i in 0 to pkg_count
+        install_count = resolver.resolve(packages[i], to_install, install_count)
+    end for
+
+    // Filter out already installed
+    let needed: [string] = new [string](100)
+    mut needed_count = 0
+    for i in 0 to install_count
+        if not database.is_installed(to_install[i])
+            needed[needed_count] = to_install[i]
+            needed_count = needed_count + 1
+        end if
+    end for
+
+    if needed_count == 0
+        print_info("All packages are already installed")
+        return
+    end if
+
+    // Confirm with user
+    if not prompt.confirm_packages("install", needed, needed_count)
+        println("Aborted.")
+        return
+    end if
+
+    let cfg = config.load()
+
+    // Install each package
+    for i in 0 to needed_count
+        let pkg_name = needed[i]
+        println("")
+        println(bold("==> Installing ${pkg_name}"))
+
+        // Try to get binary package from repository first
+        let remote_pkg = repository.find_package(pkg_name)
+
+        if str.length(remote_pkg.filename) > 0
+            // Binary package available
+            print_info("Downloading ${pkg_name}...")
+            let pkg_path = repository.download_package(remote_pkg)
+
+            if str.length(pkg_path) > 0
+                print_info("Installing ${pkg_name}...")
+                if install_package(pkg_path, pkg_name, opts)
+                    print_success("Installed ${pkg_name}")
+                    continue
+                end if
+            end if
+        end if
+
+        // Binary not available or download failed - try source
+        let build_from_source = false
+        if cfg.build.fallback_to_source == "yes"
+            build_from_source = true
+        elif cfg.build.fallback_to_source == "ask"
+            build_from_source = prompt.ask_build_from_source(pkg_name)
+        end if
+
+        if not build_from_source
+            print_error("Package not available: ${pkg_name}")
+            return
+        end if
+
+        // Find port and build
+        let p = port.find(pkg_name)
+        if str.length(p.info.name) == 0
+            print_error("Port not found: ${pkg_name}")
+            return
+        end if
+
+        print_info("Building ${pkg_name}...")
+        let build_result = build_package(p, opts)
+        if not build_result.success
+            print_error("Build failed: ${build_result.error_message}")
+            return
+        end if
+
+        print_info("Installing ${pkg_name}...")
+        if not install_package(build_result.package_path, pkg_name, opts)
+            print_error("Installation failed")
+            return
+        end if
+
+        print_success("Installed ${pkg_name}")
+    end for
+
+    println("")
+    print_success("Installation complete!")
+end run
+```
+
+---
+
+## Directory Constants
+
+```reef
+// Zygaena/Coral paths
+const PORTS_DIR: string = "/usr/ports"
+const SOURCES_DIR: string = "/var/lib/coral/sources"
+const PACKAGES_DIR: string = "/var/lib/coral/packages"
+const REPOS_DIR: string = "/var/lib/coral/repos"
+const WORK_DIR: string = "/var/tmp/coral/work"
+const PKG_DIR: string = "/var/tmp/coral/pkg"
+const DB_DIR: string = "/var/lib/coral/installed"
+const KEYS_DIR: string = "/etc/coral/keys"
+const CONFIG_FILE: string = "/etc/coral/coral.conf"
+const REPOS_CONFIG_DIR: string = "/etc/coral/repos.d"
+```
+
+---
+
+## Package Format
+
+### Port Structure
+```
+/usr/ports/<category>/<package>/
+├── package.toml          # Package metadata (TOML)
+├── build.reef            # Build commands (Reef)
+├── files/                # Additional files (optional)
+│   └── config.example
+├── patches/              # Source patches (optional)
+│   └── fix-illumos.patch
+└── scripts/              # Install scripts (optional)
+    ├── pre-install.reef
+    ├── post-install.reef
+    ├── pre-remove.reef
+    └── post-remove.reef
+```
+
+### package.toml Example
+```toml
+[package]
+name = "vim"
+version = "9.1.0"
+release = 1
+description = "Vi IMproved - enhanced vi editor"
+url = "https://www.vim.org"
+license = "Vim"
+maintainer = "[email protected]"
+
+# Alternative sets this package belongs to
+alternatives = ["vi", "editor"]
+
+# Packages this conflicts with (cannot be installed together)
+conflicts = ["vim-minimal"]
+
+[dependencies]
+runtime = ["ncurses"]
+build = ["ncurses-dev"]
+optional = [
+    "python:Python scripting support",
+    "lua:Lua scripting support"
+]
+
+[sources]
+urls = [
+    "https://github.com/vim/vim/archive/v${version}.tar.gz"
+]
+checksums = [
+    "abc123def456..."
+]
+
+[patches]
+# Patches are applied in order listed, after source extraction
+# Files are relative to the patches/ directory in the port
+files = [
+    "fix-illumos-termios.patch",
+    "fix-ncurses-detection.patch"
+]
+# Strip level for patch command (default: 1)
+strip = 1
+
+[config]
+# Config files to protect during upgrades
+# These are installed as <path>.pkg, user's version preserved as <path>
+files = [
+    "/etc/vim/vimrc"
+]
+
+[scripts]
+# Install scripts (Reef files in scripts/ directory)
+pre_install = "pre-install.reef"
+post_install = "post-install.reef"
+pre_remove = "pre-remove.reef"
+post_remove = "post-remove.reef"
+
+[build]
+parallel = true
+jobs = 0
+```
+
+### build.reef Example
+```reef
+// Build script for vim
+
+fn configure(): string
+    return "./configure --prefix=/usr --with-features=huge"
+end configure
+
+fn build(): string
+    return "gmake -j${JOBS}"
+end build
+
+fn install(): string
+    return "gmake install DESTDIR=${PKG}"
+end install
+```
+
+### Package Groups (Meta-packages)
+
+Package groups are collections of packages that can be installed together as a bundle.
+Useful for large products like desktop environments or curated package sets.
+
+Groups are defined in `/usr/ports/groups/` as simple TOML files:
+
+```toml
+# /usr/ports/groups/kde6.toml
+[group]
+name = "kde6"
+description = "KDE Plasma 6 Desktop Environment"
+
+[packages]
+# All packages installed when user runs: coral install @kde6
+members = [
+    "kde-plasma6",
+    "kde-kwin",
+    "kde-extras",
+    "kde-apps-base",
+    "sddm"
+]
+```
+
+Usage:
+```
+$ coral install @kde6
+Installing group 'kde6' (5 packages):
+  kde-plasma6, kde-kwin, kde-extras, kde-apps-base, sddm
+
+Proceed? [Y/n]
+```
+
+The `@` prefix indicates a group name rather than a package name.
+
+### Alternatives (Dependency Options)
+
+Alternatives allow a dependency to be satisfied by one of several packages.
+Packages declare which alternative sets they belong to, and dependencies
+can require any member of that set.
+
+```toml
+# In postfix/package.toml
+[package]
+name = "postfix"
+alternatives = ["mta"]    # Member of the "mta" alternative set
+
+# In opensmtpd/package.toml
+[package]
+name = "opensmtpd"
+alternatives = ["mta"]    # Also a member of "mta"
+
+# In mailman/package.toml - needs ANY mta
+[dependencies]
+runtime = ["mta:postfix,opensmtpd,sendmail"]  # Pick one from this list
+```
+
+When installing a package with alternatives:
+```
+$ coral install mailman
+
+Resolving dependencies...
+Package 'mailman' requires 'mta' (one of: postfix, opensmtpd, sendmail)
+
+No provider for 'mta' is installed.
+Available options:
+  1) postfix
+  2) opensmtpd
+  3) sendmail
+
+Select [1-3]: 2
+
+Packages to install:
+  opensmtpd
+  mailman
+
+Proceed? [Y/n]
+```
+
+If any member of the alternative set is already installed, the dependency is satisfied automatically.
+
+### Conflict Detection
+
+Coral prevents file clobbering through two mechanisms:
+
+1. **Explicit conflicts** - Declared in package.toml:
+   ```toml
+   conflicts = ["vim-minimal", "vim-tiny"]
+   ```
+
+2. **File ownership tracking** - The installed package database tracks which package owns each file. During install:
+   - If a file exists and is owned by another package → **error** (unless `--force`)
+   - If a file exists and is not owned by any package → **warning** (orphan file)
+
+### Config File Protection
+
+Config files listed in `[config].files` are protected during install/upgrade:
+
+| Scenario | Behavior |
+|----------|----------|
+| **Fresh install** | Package file installed as `/etc/foo.conf.pkg`, user copies to `/etc/foo.conf` |
+| **Upgrade, user hasn't modified** | Replace both `.conf` and `.conf.pkg` |
+| **Upgrade, user has modified** | Update `.conf.pkg` only, preserve user's `.conf` |
+| **Remove** | Remove `.conf.pkg`, warn if `.conf` differs |
+
+This ensures the user's customizations are never clobbered. The `.pkg` file always contains the package maintainer's version for reference.
+
+### Install Scripts
+
+Install scripts are Reef programs executed at specific points:
+
+| Script | When | Use Cases |
+|--------|------|-----------|
+| `pre-install.reef` | Before files extracted | Create users/groups, stop services |
+| `post-install.reef` | After files installed | Run ldconfig, enable service, print notes |
+| `pre-remove.reef` | Before files removed | Stop services, backup data |
+| `post-remove.reef` | After files removed | Remove users, cleanup state |
+
+Example `post-install.reef`:
+```reef
+// Post-install script for nginx
+
+proc main()
+    // Run ldconfig to register shared libraries
+    let pid = process.process_spawn_shell("/usr/sbin/ldconfig")
+    process.process_wait(pid)
+
+    // Print post-install notes
+    println("")
+    println("nginx installed successfully!")
+    println("Enable with: svcadm enable nginx")
+    println("Config file: /etc/nginx/nginx.conf.pkg")
+    println("Copy to /etc/nginx/nginx.conf and customize.")
+end main
+```
+
+### Binary Package (.pkg.tar.xz)
+```
+vim-9.1.0-1.pkg.tar.xz
+├── .PKGINFO              # Package metadata (TOML)
+├── .FOOTPRINT            # File listing with permissions
+├── .SCRIPTS/             # Install scripts (if any)
+│   ├── pre-install.reef
+│   └── post-install.reef
+├── usr/
+│   ├── bin/vim
+│   ├── share/vim/
+│   └── share/man/
+```
+
+---
+
+## Implementation Schedule
+
+### Phase 1: Core Infrastructure (Week 1-2)
+- [ ] Create directory structure
+- [ ] Set up reef.toml
+- [ ] Implement types.reef
+- [ ] Implement util/color.reef
+- [ ] Implement util/prompt.reef
+- [ ] Implement core/config.reef (configuration loading)
+- [ ] Implement main.reef (CLI dispatch)
+- [ ] Test net.http on illumos, report issues
+
+### Phase 2: Build System (Week 3-4)
+- [ ] Implement core/port.reef (TOML parsing)
+- [ ] Implement util/http.reef (downloads)
+- [ ] Implement util/checksum.reef (SHA256)
+- [ ] Implement util/archive.reef (tar/xz)
+- [ ] Implement commands/build.reef
+- [ ] Test with hello port
+
+### Phase 3: Package Management (Week 5-6)
+- [ ] Implement core/database.reef
+- [ ] Implement core/package.reef
+- [ ] Implement commands/install.reef
+- [ ] Implement commands/remove.reef
+- [ ] Implement commands/upgrade.reef
+- [ ] Implement commands/cache.reef
+
+### Phase 4: Query Commands (Week 7)
+- [ ] Implement commands/info.reef
+- [ ] Implement commands/list.reef
+- [ ] Implement commands/files.reef
+- [ ] Implement commands/owner.reef
+- [ ] Implement commands/search.reef
+
+### Phase 5: Repository System (Week 8-9)
+- [ ] Implement core/repository.reef (manifest parsing)
+- [ ] Implement core/signing.reef (Ed25519)
+- [ ] Implement commands/sync.reef (auto-detect git vs hg)
+- [ ] Implement commands/repo.reef (all subcommands)
+- [ ] Implement commands/key.reef (all subcommands)
+
+### Phase 6: Advanced Features (Week 10)
+- [ ] Implement core/resolver.reef (dependency resolution)
+- [ ] Implement commands/depends.reef
+- [ ] Implement commands/diff.reef
+- [ ] Error handling improvements
+- [ ] Testing with multiple packages
+
+### Phase 7: Polish & Documentation (Week 11-12)
+- [ ] Comprehensive testing
+- [ ] Man page generation
+- [ ] User documentation
+- [ ] Repository setup guide
+- [ ] Third-party repo guide
+
+---
+
+## Test Packages
+
+| Package | Category | Dependencies | Notes |
+|---------|----------|--------------|-------|
+| hello | core | None | GNU Hello, autotools |
+| tree | base | None | Single C file |
+| ncurses | devel | None | Library, needed by vim |
+| vim | base | ncurses | Tests dependency resolution |
+| jq | base | oniguruma | JSON processor |
+
+---
+
+## Success Criteria
+
+### MVP (Minimum Viable Product)
+- [ ] `coral build` creates a package from a port
+- [ ] `coral install` installs packages with dependencies
+- [ ] `coral remove` removes packages
+- [ ] `coral list` shows installed packages
+- [ ] `coral search` finds ports
+- [ ] Colored output works
+- [ ] Confirmation prompts work
+
+### Full Release
+- [ ] All subcommands functional (build, install, remove, upgrade, info, list, files, owner, search, depends, sync, diff, repo, key, cache)
+- [ ] Repository manifest system working
+- [ ] Package signing and verification working
+- [ ] Binary package download from mirrors working
+- [ ] Source fallback working with user prompts
+- [ ] 10+ packages buildable
+- [ ] Complex dependency chains work
+- [ ] Native HTTP working (or issues reported)
+- [ ] Clean error messages
+- [ ] Ready for Stage 2 (Core Ports)
+
+---
+
+## Distribution Infrastructure
+
+### Official Repository Structure
+
+```
+https://pkg.zygaena.org/
+├── stable/
+│   └── x86_64/
+│       ├── repo.toml           # Repository manifest
+│       ├── repo.toml.sig       # Manifest signature
+│       └── packages/
+│           ├── bash-5.2.21-1.pkg.tar.xz
+│           ├── bash-5.2.21-1.pkg.tar.xz.sig
+│           └── ...
+├── testing/
+│   └── x86_64/
+│       └── ...
+└── keys/
+    ├── official-2026.pub       # Current signing key
+    └── official-2025.pub       # Previous key (for verification)
+```
+
+### Mirror Sync Process
+
+1. **Build Server** (CI/CD):
+   - Builds packages from ports
+   - Signs packages with official key
+   - Generates repo.toml manifest
+   - Signs manifest
+   - Uploads to master server
+
+2. **Master Server**:
+   - Authoritative package repository
+   - Accepts uploads from build server only
+   - Serves rsync to mirrors
+
+3. **Mirror Servers**:
+   - Sync from master via rsync
+   - Serve packages via HTTP/HTTPS
+   - Listed in official mirror list
+
+### ISO/USB Distribution
+
+For offline installation or bootstrapping:
+
+```bash
+# Export repository for ISO image
+coral repo export /srv/packages /mnt/iso/packages
+
+# Creates:
+# /mnt/iso/packages/
+# ├── repo.toml
+# ├── repo.toml.sig
+# └── packages/
+#     └── *.pkg.tar.xz
+```
+
+---
+
+## Notes for Reef Team
+
+### net.http Testing on illumos
+Will test the following and report issues:
+1. Basic HTTP GET request
+2. HTTPS (TLS) support
+3. Redirect handling (301, 302)
+4. Large file downloads
+5. Connection timeout handling
+6. Progress callbacks (if supported)
+
+### Potential illumos-specific issues
+- Socket syscall constants
+- DNS resolution paths
+- TLS library linking
+
+### crypto.ed25519 Requirements
+If not available in Reef stdlib, we will use the `signify` command-line tool.
+This is acceptable as signify is a standard tool on BSD/illumos systems.
+
+### Shell-out Acceptability
+The following shell-outs are acceptable and expected:
+- `tar` / `xz` - Archive operations (standard system tools)
+- `patch` - Apply source patches with configurable strip level
+- `signify` - Ed25519 signing (until Reef adds native support)
+- `git` / `hg` - Ports collection sync (auto-detect VCS type)
+
+---
+
+## Revision History
+
+| Version | Date | Changes |
+|---------|------|---------|
+| 1.0 | January 2026 | Initial plan created |
+| 2.0 | January 2026 | Added configuration, repository system, signing, key management, cache management |
+| 2.1 | January 2026 | Clarified native vs shell-out; confirmed SHA256 is native via crypto.sha256; added Mercurial support for ports sync |
+| 2.2 | January 2026 | Added [patches] section to package.toml with ordered patch files and strip level |
+| 2.3 | January 2026 | Added conflict detection, config file protection (.pkg), and install scripts |
+| 2.4 | January 2026 | Clarified package groups (meta-packages) vs alternatives (dependency options) |
+| 2.5 | January 2026 | Added Reef language notes; Fixed syntax (run -> execute is reserved keyword); Updated module paths; Verified against Reef 0.1.3 spec |
diff -r 000000000000 -r d0a96d73e9e1 docs/STAGE2_CORE_PORTS_PLAN.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/STAGE2_CORE_PORTS_PLAN.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,251 @@
+# Stage 2: Core Ports Implementation Plan
+
+**Document Version:** 1.0
+**Created:** January 2026
+**Status:** Planning
+**Prerequisites:** Stage 1 Complete (Coral package manager validated)
+
+---
+
+## Overview
+
+Stage 2 creates the foundational ports collection for Zygaena. These are real, working packages that can be built and installed using Coral on illumos.
+
+**Goal:** Build a self-hosting development environment where Zygaena can compile itself.
+
+---
+
+## Port Categories
+
+| Category | Purpose | Examples |
+|----------|---------|----------|
+| **core** | Essential system packages | libc, zlib, ncurses, libressl |
+| **base** | Standard Unix utilities | bash, vim, curl, git, tree |
+| **devel** | Development tools & libraries | gmake, pkgconf, autoconf, reef |
+
+---
+
+## Implementation Phases
+
+### Phase 1: Zero-Dependency Ports
+
+Start with packages that have no external dependencies and are easy to build on illumos.
+
+| Port | Version | Description | Notes |
+|------|---------|-------------|-------|
+| `core/zlib` | 1.3.1 | Compression library | Foundation for many packages |
+| `base/tree` | 2.1.1 | Directory listing | Simple C, good test |
+| `base/less` | 643 | Pager | Simple, useful immediately |
+| `base/which` | 2.21 | Command locator | Single file |
+
+### Phase 2: Core Libraries
+
+Libraries needed by most other packages.
+
+| Port | Version | Dependencies | Notes |
+|------|---------|--------------|-------|
+| `core/ncurses` | 6.4 | none | Terminal handling |
+| `core/readline` | 8.2 | ncurses | Line editing for bash/shells |
+| `core/libressl` | 3.9.2 | none | TLS/SSL (preferred over openssl) |
+| `core/libffi` | 3.4.6 | none | Foreign function interface |
+| `core/libedit` | 3.1 | ncurses | BSD line editor alternative |
+
+### Phase 3: Base Utilities
+
+Essential command-line tools.
+
+| Port | Version | Dependencies | Notes |
+|------|---------|--------------|-------|
+| `base/bash` | 5.2.21 | ncurses, readline | Primary shell |
+| `base/vim` | 9.1 | ncurses | Editor |
+| `base/curl` | 8.6.0 | libressl, zlib | HTTP client |
+| `base/git` | 2.43.0 | curl, zlib, libressl | Version control |
+| `base/tmux` | 3.4 | ncurses, libevent | Terminal multiplexer |
+| `base/htop` | 3.3.0 | ncurses | Process viewer |
+
+### Phase 4: Development Tools
+
+Tools needed to build software.
+
+| Port | Version | Dependencies | Notes |
+|------|---------|--------------|-------|
+| `devel/gmake` | 4.4.1 | none | GNU Make |
+| `devel/pkgconf` | 2.1.0 | none | Package config tool |
+| `devel/autoconf` | 2.72 | none | Configure script generator |
+| `devel/automake` | 1.16.5 | autoconf | Makefile generator |
+| `devel/libtool` | 2.4.7 | none | Library build tool |
+| `devel/cmake` | 3.28.1 | curl, zlib | Build system |
+
+### Phase 5: Reef Language
+
+Package Reef itself so Zygaena can build Coral.
+
+| Port | Version | Dependencies | Notes |
+|------|---------|--------------|-------|
+| `devel/ocaml` | 5.1.1 | none | OCaml compiler (for building Reef) |
+| `devel/opam` | 2.1.5 | ocaml | OCaml package manager |
+| `devel/reef` | 0.1.4 | ocaml, opam | Reef language compiler |
+| `core/coral` | 1.0.0 | reef | Package manager itself |
+
+---
+
+## Port Structure Template
+
+Each port follows this structure:
+
+```
+/usr/ports/<category>/<name>/
+├── package.toml      # Package metadata
+├── build.sh          # Build script (shell for now, reef later)
+├── patches/          # illumos compatibility patches (if needed)
+│   └── *.patch
+└── files/            # Additional files (configs, services)
+```
+
+### Example: core/zlib/package.toml
+
+```toml
+[package]
+name = "zlib"
+version = "1.3.1"
+release = 1
+description = "General-purpose compression library"
+url = "https://zlib.net"
+license = "Zlib"
+maintainer = "Chris Tusa <chris.tusa@leafscale.com>"
+arch = "x86_64"
+
+[dependencies]
+runtime = []
+build = []
+
+[sources]
+urls = ["https://zlib.net/zlib-1.3.1.tar.gz"]
+checksums = ["9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23"]
+
+[build]
+parallel = true
+jobs = 0
+```
+
+### Example: core/zlib/build.sh
+
+```bash
+#!/bin/bash
+# Build script for zlib
+
+set -e
+
+# Environment provided by coral:
+# $SRCDIR - extracted source directory
+# $PKGDIR - installation destination (DESTDIR)
+# $JOBS   - parallel job count
+
+cd "$SRCDIR"
+
+./configure --prefix=/usr
+
+gmake -j$JOBS
+gmake install DESTDIR="$PKGDIR"
+```
+
+---
+
+## illumos Compatibility Notes
+
+### Common Issues & Fixes
+
+| Issue | Solution |
+|-------|----------|
+| `make` vs `gmake` | Always use `gmake`, set `MAKE=gmake` |
+| `tar` vs `gtar` | Always use `gtar`, set `TAR=gtar` |
+| Missing `d_type` in dirent | Apply compatibility patch |
+| Different socket API | May need `-lsocket -lnsl` |
+| No `/proc/self/exe` | Use `getexecname()` instead |
+| Terminal ioctl differences | Patch with illumos-specific code |
+
+### Build Environment Variables
+
+```bash
+export PATH="/usr/gnu/bin:$PATH"
+export CC=gcc
+export CXX=g++
+export MAKE=gmake
+export TAR=gtar
+export CFLAGS="-O2 -pipe -march=x86-64"
+export LDFLAGS=""
+```
+
+---
+
+## Dependency Graph (Simplified)
+
+```
+                    zlib
+                     |
+    +-------+--------+--------+
+    |       |                 |
+  curl    git              cmake
+    |       |
+    +---+---+
+        |
+     libressl
+
+    ncurses
+       |
+   +---+---+---+
+   |   |   |   |
+ bash vim tmux htop
+   |
+readline
+```
+
+---
+
+## Success Criteria
+
+### Minimum Viable Ports (MVP)
+- [ ] 5 working ports that build and install
+- [ ] Dependency chain works (e.g., ncurses -> vim)
+- [ ] Ports work on OmniOS test machine
+
+### Full Stage 2
+- [ ] 20+ ports covering core, base, devel categories
+- [ ] Reef can be built as a port
+- [ ] Coral can be built as a port
+- [ ] Self-hosting: can rebuild entire ports tree from itself
+- [ ] Binary repository with pre-built packages
+
+---
+
+## Implementation Order
+
+1. **Set up ports tree structure** on illumos
+2. **Create zlib port** - simplest, no deps, widely needed
+3. **Create tree port** - simple C program, validate workflow
+4. **Create ncurses port** - core library
+5. **Create vim port** - depends on ncurses, user-facing
+6. **Create libressl port** - TLS for curl/git
+7. **Create curl port** - depends on libressl
+8. **Create git port** - depends on curl
+9. **Package Reef and Coral** - self-hosting milestone
+
+---
+
+## Verification Plan
+
+After each port:
+1. `coral build <port>` - Verify it builds
+2. `coral install <package>` - Verify it installs
+3. Test the installed program works
+4. `coral remove <package>` - Verify clean removal
+
+---
+
+## Notes
+
+- Start with shell build scripts (build.sh) for simplicity
+- Convert to Reef build scripts (build.reef) later if desired
+- Focus on getting packages working, not perfection
+- Document any illumos-specific patches needed
+- Keep patch files in the port for reproducibility
diff -r 000000000000 -r d0a96d73e9e1 docs/coral.1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/coral.1	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,416 @@
+.\" Man page for coral(1)
+.\" Copyright (C) 2025 Leafscale, LLC
+.TH CORAL 1 "January 2026" "Coral 0.2.2" "Zygaena Package Manager"
+
+.SH NAME
+coral \- package manager for Zygaena
+
+.SH SYNOPSIS
+.B coral
+.RI [ options ]
+.I command
+.RI [ arguments ]
+
+.SH DESCRIPTION
+.B coral
+is a ports-based package manager for Zygaena (illumos). It can build packages
+from source ports, install binary packages from repositories, manage
+dependencies, and handle package signing and verification.
+
+.SH COMMANDS
+
+.SS "Package Operations"
+
+.TP
+.BI "build " port
+Build a package from a port. The port name should match a directory under
+/usr/ports/<category>/<port>. The resulting package is stored in
+/var/lib/coral/packages/.
+
+.TP
+.BI "install " "package ..."
+Install one or more packages. Packages can be specified as:
+.RS
+.IP \(bu 2
+Package name (downloaded from repository or built locally)
+.IP \(bu 2
+Path to a .pkg.tar.xz file
+.IP \(bu 2
+Group name prefixed with @ (e.g., @base-devel)
+.RE
+
+.TP
+.BI "remove " "package ..."
+Remove one or more installed packages. Runs pre-remove and post-remove
+scripts if present.
+
+.TP
+.BI "upgrade " "[package ...]"
+Upgrade installed packages to newer versions. If no packages are specified,
+upgrades all packages with available updates.
+
+.SS "Query Operations"
+
+.TP
+.BI "info " package
+Display detailed information about a package, including version, description,
+dependencies, and installation status.
+
+.TP
+.B list
+List all installed packages with their versions.
+
+.TP
+.BI "files " package
+List all files owned by an installed package.
+
+.TP
+.BI "owner " file
+Find which package owns a given file path.
+
+.TP
+.BI "search " term
+Search for packages matching the given term in ports and repositories.
+
+.TP
+.BI "depends " package
+Display the dependency tree for a package.
+
+.TP
+.B diff
+Show packages that have newer versions available.
+
+.SS "Repository Operations"
+
+.TP
+.B sync
+Synchronize the local ports tree with the remote repository (git pull).
+
+.TP
+.BI "repo " subcommand
+Manage package repositories. Subcommands:
+.RS
+.TP
+.B list
+List configured repositories
+.TP
+.B refresh
+Refresh repository metadata
+.TP
+.BI "add " "name url"
+Add a new repository
+.TP
+.BI "remove " name
+Remove a repository
+.TP
+.BI "init " path
+Initialize a new repository structure
+.TP
+.BI "rebuild " path
+Rebuild repository manifest from packages
+.TP
+.BI "sign " path
+Sign repository manifest
+.TP
+.BI "verify " path
+Verify repository signature
+.RE
+
+.SS "Key Management"
+
+.TP
+.BI "key " subcommand
+Manage signing keys. Subcommands:
+.RS
+.TP
+.B list
+List keys in the keyring
+.TP
+.BI "generate " name
+Generate a new Ed25519 signing key
+.TP
+.BI "import " "file name"
+Import a public key
+.TP
+.BI "export " name
+Export a public key
+.TP
+.BI "trust " name
+Add key to trusted keys
+.TP
+.BI "revoke " name
+Remove key from trusted keys
+.TP
+.B init
+Initialize the keyring directory
+.RE
+
+.SS "Cache Management"
+
+.TP
+.BI "cache " subcommand
+Manage package caches. Subcommands:
+.RS
+.TP
+.B clean
+Remove cached packages
+.TP
+.B clean-sources
+Remove cached source archives
+.TP
+.B clean-all
+Remove all cached data
+.TP
+.B stats
+Show cache statistics
+.RE
+
+.SS "Database Management"
+
+.TP
+.BI "db " subcommand
+Manage the package database indexes. The database provides fast O(log N) lookups
+for file ownership and package information. Subcommands:
+.RS
+.TP
+.B init
+Initialize the database indexes. Creates packages.db and files.db in
+/var/lib/coral/db/. Run this once during initial system setup.
+.TP
+.B rebuild
+Rebuild indexes from the installed package database. The source of truth
+is always /var/lib/coral/installed/; indexes can be safely rebuilt at any time.
+Use this if indexes become corrupted or out of sync.
+.TP
+.B status
+Display database status including number of indexed packages and files.
+.TP
+.B dump
+Display the contents of the files index (for debugging).
+.RE
+
+.SH OPTIONS
+
+.TP
+.BR \-h ", " \-\-help
+Display help message and exit.
+
+.TP
+.B \-\-version
+Display version information and exit.
+
+.TP
+.BR \-y ", " \-\-yes
+Assume yes to all prompts. Do not ask for confirmation.
+
+.TP
+.BR \-f ", " \-\-force
+Force operation. For install, reinstalls even if already installed.
+For remove, removes even with dependents.
+
+.TP
+.BR \-v ", " \-\-verbose
+Enable verbose output. Show additional details during operations.
+
+.TP
+.BR \-n ", " \-\-dry\-run
+Show what would be done without making changes.
+
+.TP
+.BR \-q ", " \-\-quiet
+Minimal output. Only show errors.
+
+.TP
+.B \-\-no\-color
+Disable colored output.
+
+.TP
+.BI \-\-root " path"
+Use an alternative root directory for installation.
+
+.SH FILES
+
+.TP
+.I /etc/coral/coral.conf
+Main configuration file.
+
+.TP
+.I /etc/coral/repos.d/
+Repository configuration files (*.repo).
+
+.TP
+.I /etc/coral/keys/
+Signing keys (private and public).
+
+.TP
+.I /usr/ports/
+Ports tree containing package build definitions.
+
+.TP
+.I /usr/ports/groups/
+Package group definitions (*.toml).
+
+.TP
+.I /var/lib/coral/installed/
+Database of installed packages (source of truth).
+
+.TP
+.I /var/lib/coral/db/
+Package database indexes for fast lookups.
+
+.TP
+.I /var/lib/coral/db/packages.db
+MsgPack index of installed package metadata.
+
+.TP
+.I /var/lib/coral/db/files.db
+MsgPack index mapping files to their owning packages (enables O(log N) lookups).
+
+.TP
+.I /var/lib/coral/packages/
+Cached binary packages.
+
+.TP
+.I /var/lib/coral/sources/
+Cached source archives.
+
+.TP
+.I /var/lib/coral/repos/
+Cached repository manifests.
+
+.TP
+.I /var/lib/coral/trusted-keys/
+Trusted public keys for verification.
+
+.TP
+.I /var/tmp/coral/work/
+Build work directory.
+
+.TP
+.I /var/tmp/coral/pkg/
+Package staging directory.
+
+.SH CONFIGURATION
+The configuration file /etc/coral/coral.conf uses TOML format:
+
+.nf
+[general]
+arch = "x86_64"
+jobs = 0              # 0 = auto-detect
+confirm = true
+color = true
+
+[paths]
+ports = "/usr/ports"
+packages = "/var/lib/coral/packages"
+
+[build]
+cflags = "-O2 -pipe"
+strip = true
+
+[security]
+require_signed_repos = false
+require_signed_packages = false
+.fi
+
+.SH EXAMPLES
+
+Build and install a package:
+.nf
+    coral build vim
+    coral install vim
+.fi
+
+Install multiple packages:
+.nf
+    coral install vim nano htop
+.fi
+
+Install a package group:
+.nf
+    coral install @base-devel
+.fi
+
+Search for packages:
+.nf
+    coral search editor
+.fi
+
+Show package dependencies:
+.nf
+    coral depends vim
+.fi
+
+Find which package owns a file:
+.nf
+    coral owner /usr/bin/vim
+.fi
+
+Upgrade all packages:
+.nf
+    coral upgrade
+.fi
+
+Generate a signing key:
+.nf
+    coral key generate mykey
+.fi
+
+Sign a repository:
+.nf
+    coral repo sign /path/to/repo
+.fi
+
+Initialize the package database:
+.nf
+    coral db init
+.fi
+
+Check database status:
+.nf
+    coral db status
+.fi
+
+Rebuild database indexes (if corrupted):
+.nf
+    coral db rebuild
+.fi
+
+.SH EXIT STATUS
+.TP
+.B 0
+Success.
+.TP
+.B 1
+General error.
+.TP
+.B 2
+Command line usage error.
+.TP
+.B 3
+Package not found.
+.TP
+.B 4
+Dependency resolution failed.
+.TP
+.B 5
+Build failed.
+.TP
+.B 6
+Permission denied (not root).
+
+.SH SEE ALSO
+.BR coral.conf (5),
+.BR coral-build (7),
+.BR coral-repo (7)
+
+.SH BUGS
+Report bugs at https://github.com/leafscale/zygaena/issues
+
+.SH AUTHORS
+Chris Tusa <chris.tusa@leafscale.com>
+.br
+Leafscale, LLC \- https://www.leafscale.com
+
+.SH COPYRIGHT
+Copyright (C) 2025 Leafscale, LLC. Licensed under BSD-2-Clause.
diff -r 000000000000 -r d0a96d73e9e1 docs/git-history.csv
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/git-history.csv	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,121 @@
+commit,short,date,author,email,subject

+b03ff6660806c4a313e230d4d3a8cd0b3ead96ab,b03ff66,2026-01-25T18:21:56-06:00,Chris Tusa,chris.tusa@leafscale.com,Initial commit: Coral package manager for Zygaena

+b6fcf6c0913b31534a91ee1b8f2ca1f1ad8391e6,b6fcf6c,2026-01-25T18:26:46-06:00,Chris Tusa,chris.tusa@leafscale.com,Add release script

+a9e5b80d4419658fe77405ad983b23bbb6e38d2e,a9e5b80,2026-01-25T18:31:58-06:00,Chris Tusa,chris.tusa@leafscale.com,updated gitignore to exclude releases/ binary

+4b81063d9eb0b7bdc637d1b4f9278ea9c6fa7c78,4b81063,2026-01-26T22:54:49-06:00,Chris Tusa,chris.tusa@leafscale.com,Update http.reef for Reef 0.1.8 native downloads

+cc6f827268cc6648983d5cd764c73d4caa162376,cc6f827,2026-01-26T17:36:52-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix http.reef for Reef 0.1.7 compatibility

+ce380ed668d8c0576c8a616d1d177e3e28047ba9,ce380ed,2026-01-26T17:41:36-06:00,Chris Tusa,chris.tusa@leafscale.com,Add user confirmation prompts and improve command workflows

+3acc88aa18929868459745b0415f49e44fe51827,3acc88a,2026-01-26T18:12:02-06:00,Chris Tusa,chris.tusa@leafscale.com,Add --root and --prefix support for alternate root installation

+22468ffc544902769dc01e151c47c97b4013cf89,22468ff,2026-01-26T18:17:04-06:00,Chris Tusa,chris.tusa@leafscale.com,Add rooted resolver for alternate root installation

+199584532c5901135ddfef58d022396b11696766,1995845,2026-01-26T18:29:13-06:00,Chris Tusa,chris.tusa@leafscale.com,Add curl fallback for HTTP downloads on illumos

+88e57e2a80b58a2036449006e8b80bafac2c78db,88e57e2,2026-01-26T19:54:26-06:00,Chris Tusa,chris.tusa@leafscale.com,Add Woodpecker CI pipeline for build and release

+293b7610911fdbceba68e9bc53aa345e360018b8,293b761,2026-01-26T19:55:29-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix Woodpecker CI pipeline for v3.12.0 compatibility

+1bdcbc314a21d54b29f2e064c2ba3daaa491ffdf,1bdcbc3,2026-01-26T19:56:57-06:00,Chris Tusa,chris.tusa@leafscale.com,Add authentication to Reef compiler download

+0b632431dd01bd70dcf4e47e202ca9021df48bf0,0b63243,2026-01-26T20:19:57-06:00,Chris Tusa,chris.tusa@leafscale.com,Debug secrets and try alternative environment syntax

+b460a2d41f4dd71cdccb001b7e1581460f49393c,b460a2d,2026-01-26T20:22:02-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix YAML syntax for environment secrets

+4bda1d38b31dae23cbc9022c629aac5d2766328c,4bda1d3,2026-01-26T20:25:05-06:00,Chris Tusa,chris.tusa@leafscale.com,Try Woodpecker built-in CI_FORGE_TOKEN

+e06e651b2400207ba828cf7a13c5d170d98a3630,e06e651,2026-01-26T20:28:47-06:00,Chris Tusa,chris.tusa@leafscale.com,Add FORGEJO_TOKEN back to build step

+ffd38319b6f10cc9a4959b30080652728dcebf01,ffd3831,2026-01-26T20:32:59-06:00,Chris Tusa,chris.tusa@leafscale.com,Trigger CI to test trusted security setting

+5c069996e2b7375610215727baf70e22840329c9,5c06999,2026-01-26T20:34:38-06:00,Chris Tusa,chris.tusa@leafscale.com,Try secrets array syntax with source/target

+77b6a6d2a0bc21d508d81c2f0ab4b3faf7ac896a,77b6a6d,2026-01-26T20:35:41-06:00,Chris Tusa,chris.tusa@leafscale.com,Revert to environment/from_secret syntax

+6678097900ac1cde1b4e7bf73833c84c697ae1fe,6678097,2026-01-26T20:38:34-06:00,Chris Tusa,chris.tusa@leafscale.com,Add API debug to test reef-lang access

+d049473f63e4736dbc4190118f1d39fcf9c2d016,d049473,2026-01-26T20:43:27-06:00,Chris Tusa,chris.tusa@leafscale.com,"Add deeper token debug - show first 4 chars, write to file"

+279134f09b1a989ba75b2bd81a79d95a6b6071a8,279134f,2026-01-26T20:52:17-06:00,Chris Tusa,chris.tusa@leafscale.com,Update secret name to coral-releases

+72f3fd68f31ebdf11b8349b08eefd7089d5be0e3,72f3fd6,2026-01-26T20:53:25-06:00,Chris Tusa,chris.tusa@leafscale.com,Add test_secret to debug Woodpecker secrets

+d971e66f91c40c7631df907b5910f1192bac8bcc,d971e66,2026-01-26T20:54:54-06:00,Chris Tusa,chris.tusa@leafscale.com,TEMPORARY: hardcode token to test pipeline

+82460c8577f9b0092a0de5931306893e2f259ae5,82460c8,2026-01-26T20:58:40-06:00,Chris Tusa,chris.tusa@leafscale.com,Fresh Woodpecker pipeline from scratch

+7471b00b4f192dc3adb258c43d0c895afd805297,7471b00,2026-01-26T21:02:49-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix YAML quoting for curl commands

+3a97f201ceca288d5044fa197883a017176cfdeb,3a97f20,2026-01-26T21:03:30-06:00,Chris Tusa,chris.tusa@leafscale.com,Use curly braces for variable expansion

+15fe57988156ad967a2475c418661039e769b1ac,15fe579,2026-01-26T21:06:39-06:00,Chris Tusa,chris.tusa@leafscale.com,Try API_KEY as variable name

+c9cb48facf63d97ddf4529bd93caf1db79ce6d49,c9cb48f,2026-01-26T21:08:30-06:00,Chris Tusa,chris.tusa@leafscale.com,"Revert to reef_token secret, clean up pipeline"

+aaa7a4f57c971ef4103996e85669db8c8a354e8e,aaa7a4f,2026-01-26T21:09:23-06:00,Chris Tusa,chris.tusa@leafscale.com,Add test_secret to debug secret injection

+3a1cbd831fdbf3b695d93d661b8c61a8a49c5e9b,3a1cbd8,2026-01-26T21:27:00-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix variable escaping for Woodpecker preprocessing

+6e928a411ca49868f940fd307f068b6d17d75bf5,6e928a4,2026-01-26T21:27:44-06:00,Chris Tusa,chris.tusa@leafscale.com,Quote curl command to fix YAML parsing

+2486d042722a126716c75dff08eb6b79a957f0ad,2486d04,2026-01-26T21:28:44-06:00,Chris Tusa,chris.tusa@leafscale.com,Debug: show tarball structure

+1c191e93d4984e8bfc99c0ba30d3a6a7f3e565d2,1c191e9,2026-01-26T21:30:02-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix reefc path and clean up debug statements

+6bf7480082b3938723293cdcc958bdef16769cfd,6bf7480,2026-01-26T21:31:13-06:00,Chris Tusa,chris.tusa@leafscale.com,Switch to Ubuntu 24.04 for GLIBC 2.38+ compatibility

+f2ef479898aab95e3a478f4eb17e6e2c3ab4d654,f2ef479,2026-01-26T21:32:20-06:00,Chris Tusa,chris.tusa@leafscale.com,Set REEF_STDLIB environment variable for reefc

+ffbb7793c9819eea7efd4fbd3aafb85a8abfb210,ffbb779,2026-01-26T21:33:39-06:00,Chris Tusa,chris.tusa@leafscale.com,Use REEF_HOME instead of REEF_STDLIB

+c646c2f45e235c2713daac859a509faa244d4fcc,c646c2f,2026-01-26T21:34:25-06:00,Chris Tusa,chris.tusa@leafscale.com,Set both REEF_HOME and REEF_STDLIB

+618ab8c7ed35cd0fe7d6ebab76f5829712ab4a38,618ab8c,2026-01-26T21:35:36-06:00,Chris Tusa,chris.tusa@leafscale.com,"Debug: extract without strip, show structure"

+3a465aa5f2690e5dfdc02ed55e19f199a52952aa,3a465aa,2026-01-26T21:37:48-06:00,Chris Tusa,chris.tusa@leafscale.com,Set both REEF_HOME and REEF_STDLIB for reefc

+537795eff98516a07b184c9d3f0d21a08f3115e0,537795e,2026-01-26T21:39:03-06:00,Chris Tusa,chris.tusa@leafscale.com,Debug: check stdlib contents in binary distribution

+0bf8a2060e3b1af4f59433623dd3291add69aaab,0bf8a20,2026-01-26T21:40:04-06:00,Chris Tusa,chris.tusa@leafscale.com,Workaround: symlink stdlib to reef-stdlib (Reef packaging bug)

+79d691addc6e79d4364bd8d6b289593e516d8a93,79d691a,2026-01-26T21:41:02-06:00,Chris Tusa,chris.tusa@leafscale.com,"Set REEF_HOME in environment block, add reef-runtime symlink and gcc"

+5b95ae40fa223e8b196aa20b589dc7deeee2d9a2,5b95ae4,2026-01-26T22:48:54-06:00,Chris Tusa,chris.tusa@leafscale.com,Simplify Reef setup - just add bin to PATH

+52d81e297fdf5f522faaa533f56523f6fee51a18,52d81e2,2026-01-26T22:49:56-06:00,Chris Tusa,chris.tusa@leafscale.com,Install build-essential for C headers

+22599cf95715bb9b59b44d158ed3535ca341fe74,22599cf,2026-01-26T22:54:13-06:00,Chris Tusa,chris.tusa@leafscale.com,Bump version to 0.1.2

+017c4012259061c3d101faf25007ba3a7837cb19,017c401,2026-01-26T23:04:38-06:00,Chris Tusa,chris.tusa@leafscale.com,Add gnu and illumos categories to port search

+0318423949888750d43d046f420c1b4a284ac13f,0318423,2026-01-27T10:51:34-06:00,Chris Tusa,chris.tusa@leafscale.com,Add build cleanup options and implementation

+f3b3e306109d1528bc52d1499abcc45f2248654c,f3b3e30,2026-01-27T11:03:36-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix dependency bug

+457b3cb798fbb197bae46b57be4608227c447d70,457b3cb,2026-01-27T11:30:35-06:00,Chris Tusa,chris.tusa@leafscale.com,Add source mirrors refactor plan (Option B)

+1af9101522207a7d3e431bc128a2327e9bb75d9e,1af9101,2026-01-27T12:14:54-06:00,Chris Tusa,chris.tusa@leafscale.com,Implement source mirrors support (Option B)

+2d49d527619b5aeccb01cec1fb59b23b9d14346d,2d49d52,2026-01-27T13:15:05-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.1.3: Documentation updates and source mirrors

+295065f656a9d12f975cfd721bb1bbd76be37996,295065f,2026-01-27T15:45:15-06:00,Chris Tusa,chris.tusa@leafscale.com,Update Reef compiler to 0.1.10

+ba09e0184c81971064c76ff9ae19069c2ea01f4f,ba09e01,2026-01-27T15:48:45-06:00,Chris Tusa,chris.tusa@leafscale.com,Handle existing releases in CI pipeline

+2f44050a6abaa9493f2b862a9cb7f55b9c1b3b53,2f44050,2026-01-27T17:28:16-06:00,Chris Tusa,chris.tusa@leafscale.com,Add local source support and empty package validation

+e062adc983f4c2616dd30d29365b522ba55bf4e5,e062adc,2026-01-27T17:41:23-06:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.1.4: Local sources, empty package validation"

+de2a6793c6d2c1df8054db0237ce5b0853002e94,de2a679,2026-01-27T22:00:43-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.1.5: Build logging and output ordering fix

+39a6f92bca85838fe24528be9e02d2137708e817,39a6f92,2026-01-28T00:27:36-06:00,Chris Tusa,chris.tusa@leafscale.com,Add bug report for HTTP redirect and version selection issues

+e08240846c675b3904d67211370f7a36aa5e34c9,e082408,2026-01-28T11:33:02-06:00,Chris Tusa,chris.tusa@leafscale.com,Add bug report for source extraction hang

+c2a68d0ca690312b7eb1b7681f6c641dd8c227af,c2a68d0,2026-01-28T14:05:47-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.1.7: Bug fixes and new features

+998a2cc975a783420807c20bd0ef5f07e6cb0cdd,998a2cc,2026-01-28T15:38:39-06:00,Chris Tusa,chris.tusa@leafscale.com,Update extraction bug: XZ files still broken in 0.1.7

+e4efc01cb74e3e519596d36bb4fb276359f41b9e,e4efc01,2026-01-28T16:28:26-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.1.9: Fix XZ extraction hang with Reef 0.1.14

+d418f8e8da7482cecf355ac30dea445b9dc8f5ba,d418f8e,2026-01-28T17:41:59-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.1.10: illumos compatibility and file tracking fixes

+11a24e2ba15cb76159278e4fd8a174e222f21550,11a24e2,2026-01-29T20:22:08-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.1: Add --root support for alternate filesystem operations

+6456fce16d606342244f913a1551f148995eeec8,6456fce,2026-01-29T20:23:20-06:00,Chris Tusa,chris.tusa@leafscale.com,Remove resolved bug reports and deprecated cache command

+aac35d47153677ba767e1811fc765b9f2a584721,aac35d4,2026-01-29T20:34:49-06:00,Chris Tusa,chris.tusa@leafscale.com,Move chroot scripts to tools/ and add coral binary to gitignore

+12dcf11386c26692093183acbcc4408750d60541,12dcf11,2026-01-29T20:38:25-06:00,Chris Tusa,chris.tusa@leafscale.com,mkchroot: Generate scripts alongside chroot directory

+9478e6215aa6435f0209f6222dfd4bcc311aaebd,9478e62,2026-01-29T21:01:07-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix --root/--prefix flag parsing to work in any position

+2e8a15f2a8c502adf6f009ea5a334cb210498a35,2e8a15f,2026-01-29T21:48:55-06:00,Chris Tusa,chris.tusa@leafscale.com,Implement MsgPack-based package database indexes

+a668688fbf217971cd9499bd44f80615a655a552,a668688,2026-01-29T22:20:13-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.2: Fix MsgPack parsing and add database documentation

+8d1f81dfd4649ae7cab7090d3e7671d07ce21594,8d1f81d,2026-01-30T09:19:28-06:00,Chris Tusa,chris.tusa@leafscale.com,updated woodpecker to use reef-lang-0.1.15

+9f1c0251f55dbc9bb3b1638fe45c3152d7db6e4e,9f1c025,2026-01-30T10:21:47-06:00,Chris Tusa,chris.tusa@leafscale.com,updated gitignore

+826d764a8328b043a12c4f20ddf03b3c0e98bc7e,826d764,2026-01-30T17:31:25-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.3: Add download progress bar and allow non-root builds

+623ec00c0079403ac97a77e244c7f0bdcb8b6257,623ec00,2026-01-30T18:43:31-06:00,Chris Tusa,chris.tusa@leafscale.com,Update woodpecker CI to use reef-lang-0.1.16

+606628b29f8081499a217f3cda105514ecf56261,606628b,2026-02-19T20:38:07-06:00,Chris Tusa,chris.tusa@leafscale.com,Add hammerhead category to port search list

+2c66115aab82b148e7c3839b4c075f2bfc7c2521,2c66115,2026-02-19T20:40:02-06:00,Chris Tusa,chris.tusa@leafscale.com,Support category/name format in port find

+b416c5a9e7f826f2e17b93f773a5ea8ce6691ec3,b416c5a,2026-02-19T22:45:31-06:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.2.4: Fix resolver bug, dynamic categories, ports index, ports command"

+75d402d5455b0427e99d5b55b5f3962fc7e9f2e6,75d402d,2026-02-19T22:50:33-06:00,Chris Tusa,chris.tusa@leafscale.com,Update documentation for v0.2.4 features

+3ae110affb94f89ea99887141ecdf87e5a22441b,3ae110a,2026-02-19T23:04:47-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.5: Fix files.db sort hang on large packages

+fae09e1490c6bb57a5b36fbfb6107064790347f1,fae09e1,2026-02-19T23:17:55-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.6: Fix files.db rebuild hang from massive over-allocation

+38023ac9014277fccab175462941dce1a688f0e0,38023ac,2026-02-19T23:33:19-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.7: Fix install hang on large packages (161K+ files)

+6d0400d05640a3b2e597718e94d11ac0b435a954,6d0400d,2026-02-19T23:47:07-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.8: Fix register hang by replacing string building with shell pipeline

+373f7f46b378f7cdb32e933a120233069e913ca0,373f7f4,2026-02-19T23:56:38-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.9: Replace shell workaround with str.join() for footprint processing

+601da8e5ef047189eec94f5b620a0c9b6cb49020,601da8e,2026-03-02T23:16:32-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix example config and default arch to match Zygaena defaults

+8c6d1a8dafa3b346e92bb9efac7a669c285012fb,8c6d1a8,2026-03-03T11:55:01-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.10

+c0d1b543bcbea7f0791cecd0c017a07023c919df,c0d1b54,2026-03-03T12:12:18-06:00,Chris Tusa,chris.tusa@leafscale.com,Update CI to use reef-lang v0.3.4

+f220c00e69ffcba8b5d1f89865504d32dd6d52d0,f220c00,2026-03-03T19:45:55-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.11: Fix build script shell invocation and CPU detection

+66624612dfb38f4b6f46644ad3486fafa363b247,6662461,2026-03-07T19:47:23-06:00,Chris Tusa,chris.tusa@leafscale.com,Add mtree manifest module and replace .FOOTPRINT with .MANIFEST in build

+5289616d94fb3a9b2d72b984be505528b572b1ba,5289616,2026-03-07T19:58:29-06:00,Chris Tusa,chris.tusa@leafscale.com,Replace rsync with manifest-driven fs.ops installer

+373d4996b402e89933b1520032a4e954b225033c,373d499,2026-03-07T20:02:14-06:00,Chris Tusa,chris.tusa@leafscale.com,Add [dependencies] to .PKGINFO and update TODO status

+261fd8e618ec70bdd848d2ac69e70742d0969452,261fd8e,2026-03-07T20:06:41-06:00,Chris Tusa,chris.tusa@leafscale.com,Add config file protection on package removal

+d7c9548c735d16ec08632cb05b648d4d8fb54190,d7c9548,2026-03-07T20:40:41-06:00,Chris Tusa,chris.tusa@leafscale.com,"Rewrite script model: remove .reef type, use zsh, add upgrade flow"

+78422089d8bf212b3dec6d5a63654d6153e0618d,7842208,2026-03-07T21:06:29-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.3.0: Package Format v2 complete

+3f74bcde5e57e13a31ca28c94f064978483e6cb2,3f74bcd,2026-03-07T22:15:22-06:00,Chris Tusa,chris.tusa@leafscale.com,Auto-install missing build dependencies before compilation

+cc4f8e31c68efe5092bae574414ed63f19e84871,cc4f8e3,2026-03-07T23:10:59-06:00,Chris Tusa,chris.tusa@leafscale.com,"Export $PREFIX, $SYSCONFDIR, $PKG_ROOT to build scripts"

+02417f9862b4236f7ac76b54181c195328d205c8,02417f9,2026-03-08T00:14:11-06:00,Chris Tusa,chris.tusa@leafscale.com,Recursive build-from-source dependency resolver

+fbe0637b2dc158e1afc77202d5bd24687a2152f3,fbe0637,2026-03-08T12:09:49-05:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.3.5: Wire up all unwired features and cleanup

+b88a15a3a11cdc6401038fe3d663e2fd1c68cf5c,b88a15a,2026-03-08T15:37:57-05:00,Chris Tusa,chris.tusa@leafscale.com,Fix infinite CPU loop in packaging step

+604db08ff4f7276ebf76b4095640f76a67c05ffa,604db08,2026-03-08T20:05:35-05:00,Chris Tusa,chris.tusa@leafscale.com,Fix O(n²) manifest generation using StringBuilder (BUG-027 workaround)

+2f040691a486473343c7b2ef8a1eab5bf31032f6,2f04069,2026-03-08T23:38:23-05:00,Chris Tusa,chris.tusa@leafscale.com,Cache config paths to reduce TOML parsing allocation pressure

+c388de7ff8f4f92a33896b965fac57541cb593d7,c388de7,2026-03-08T23:40:56-05:00,Chris Tusa,chris.tusa@leafscale.com,Update CI to use Reef 0.5.2

+242ff329a523c8547db03a486611a774c589c022,242ff32,2026-03-09T16:12:57-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.3.9: Fix GC-related crashes, use self for install subprocess"

+2c7e72fcd76d8974b724825140a7fe85e769af10,2c7e72f,2026-03-09T16:44:19-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.3.10: Fix install from file path, fix conflicts array crash"

+4723f14f9809bc746a0c110284a7e91ac2a885a4,4723f14,2026-03-09T20:33:14-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.3.11: Fix reinstall/force install, add version subcommand"

+65244f3fa976aa9ee3614cf01e0a1a1ccc8b3668,65244f3,2026-03-10T16:34:48-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.3.12: Install package after build, update CI to Reef 0.5.5"

+7d9ff3efc23b38f310cd53b24e5a217abcc70ed4,7d9ff3e,2026-03-10T22:24:23-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.3.13: Fix redirect crash, illumos build compat, zero-alloc uid/gid"

+507ca32e163e73594cc22535c8b72dac6bdee890,507ca32,2026-03-11T14:49:42-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.4.0: Align build system with Hammerhead porting guide, default prefix /usr/local"

+c5956c1b204ba2a39370ec9431f208c760f7d602,c5956c1,2026-03-11T20:52:10-05:00,Chris Tusa,chris.tusa@leafscale.com,"docs: add patches section, BUILD_TRIPLE, update prefix to /usr/local"

+dcc8974f3fb937f7a5f1d1b0c9f05b44e4ddd652,dcc8974,2026-03-11T22:29:39-05:00,Chris Tusa,chris.tusa@leafscale.com,Fix package removal failing to unregister from database

+4e82c0f0cccfa2cecfebf54071392f418542775c,4e82c0f,2026-03-12T15:34:07-05:00,Chris Tusa,chris.tusa@leafscale.com,Add LDFLAGS and PKG_CONFIG_PATH to build environment

+4ff188f470ff970c73ba8bdc422425e2d72213ec,4ff188f,2026-03-12T15:35:54-05:00,Chris Tusa,chris.tusa@leafscale.com,Simplify PKG_CONFIG_PATH (avoid ${} in string literal - reefc bug)

+9b1904eb8b0ecb1d5e0c10feda09d36f2db98038,9b1904e,2026-03-12T15:39:49-05:00,Chris Tusa,chris.tusa@leafscale.com,"LDFLAGS: only set -R (RPATH), drop -L to avoid shadowing base libs"

+1f1b687238b9b16d7819d7916f295a79ba8c0377,1f1b687,2026-03-12T15:40:59-05:00,Chris Tusa,chris.tusa@leafscale.com,"docs: explain LDFLAGS, PKG_CONFIG_PATH, and library resolution design"

+ec67d5dfb3effec2ea7ff826fc8537a62314e5e1,ec67d5d,2026-04-08T18:56:43-05:00,Chris Tusa,chris.tusa@leafscale.com,fix(build): add --install/--no-install flag parsing (default: no-install)

+aefab7046c5022e9647aa94158c7940c9a6213cd,aefab70,2026-04-08T18:57:34-05:00,Chris Tusa,chris.tusa@leafscale.com,fix(build): gate target install behind --install flag (fixes bug 007)

+4ace9941b38dff3ecc442b19717df5a54ba2d7ee,4ace994,2026-04-08T18:58:22-05:00,Chris Tusa,chris.tusa@leafscale.com,docs: update build command usage for --install/--no-install flags

+b85d2815eb145523dfcaf26b131f396d6b418d03,b85d281,2026-04-08T19:29:22-05:00,Chris Tusa,chris.tusa@leafscale.com,bump version to 0.4.1

+9ffd39abc2c212b57454651a84172a9a42809c67,9ffd39a,2026-05-04T20:15:04-05:00,Chris Tusal,chris.tusa@leafscale.com,fix: compile against Reef 0.5.20 strict type checking (fixes bug 009)

+c528299ec33440d40f1ee76448cdd2c9271c8469,c528299,2026-05-04T20:15:07-05:00,Chris Tusal,chris.tusa@leafscale.com,bump version to 0.4.2

+a578ffb3457529c059f204edd65cb7364a259548,a578ffb,2026-05-04T20:24:57-05:00,Chris Tusal,chris.tusa@leafscale.com,ci: pin Reef toolchain to 0.5.20

diff -r 000000000000 -r d0a96d73e9e1 docs/git-history.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/git-history.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,127 @@
+# Coral Git History
+
+Historical record of the Coral git repository prior to the Mercurial + Isurus migration.
+Captured at migration time. Total commits: **120**.
+
+| Date | Commit | Author | Subject |
+|------|--------|--------|---------|
+| 2026-05-04T20:24:57-05:00 | `a578ffb` | Chris Tusal | ci: pin Reef toolchain to 0.5.20 |
+| 2026-05-04T20:15:07-05:00 | `c528299` | Chris Tusal | bump version to 0.4.2 |
+| 2026-05-04T20:15:04-05:00 | `9ffd39a` | Chris Tusal | fix: compile against Reef 0.5.20 strict type checking (fixes bug 009) |
+| 2026-04-08T19:29:22-05:00 | `b85d281` | Chris Tusa | bump version to 0.4.1 |
+| 2026-04-08T18:58:22-05:00 | `4ace994` | Chris Tusa | docs: update build command usage for --install/--no-install flags |
+| 2026-04-08T18:57:34-05:00 | `aefab70` | Chris Tusa | fix(build): gate target install behind --install flag (fixes bug 007) |
+| 2026-04-08T18:56:43-05:00 | `ec67d5d` | Chris Tusa | fix(build): add --install/--no-install flag parsing (default: no-install) |
+| 2026-03-12T15:40:59-05:00 | `1f1b687` | Chris Tusa | docs: explain LDFLAGS, PKG_CONFIG_PATH, and library resolution design |
+| 2026-03-12T15:39:49-05:00 | `9b1904e` | Chris Tusa | LDFLAGS: only set -R (RPATH), drop -L to avoid shadowing base libs |
+| 2026-03-12T15:35:54-05:00 | `4ff188f` | Chris Tusa | Simplify PKG_CONFIG_PATH (avoid ${} in string literal - reefc bug) |
+| 2026-03-12T15:34:07-05:00 | `4e82c0f` | Chris Tusa | Add LDFLAGS and PKG_CONFIG_PATH to build environment |
+| 2026-03-11T22:29:39-05:00 | `dcc8974` | Chris Tusa | Fix package removal failing to unregister from database |
+| 2026-03-11T20:52:10-05:00 | `c5956c1` | Chris Tusa | docs: add patches section, BUILD_TRIPLE, update prefix to /usr/local |
+| 2026-03-11T14:49:42-05:00 | `507ca32` | Chris Tusa | Release v0.4.0: Align build system with Hammerhead porting guide, default prefix /usr/local |
+| 2026-03-10T22:24:23-05:00 | `7d9ff3e` | Chris Tusa | Release v0.3.13: Fix redirect crash, illumos build compat, zero-alloc uid/gid |
+| 2026-03-10T16:34:48-05:00 | `65244f3` | Chris Tusa | Release v0.3.12: Install package after build, update CI to Reef 0.5.5 |
+| 2026-03-09T20:33:14-05:00 | `4723f14` | Chris Tusa | Release v0.3.11: Fix reinstall/force install, add version subcommand |
+| 2026-03-09T16:44:19-05:00 | `2c7e72f` | Chris Tusa | Release v0.3.10: Fix install from file path, fix conflicts array crash |
+| 2026-03-09T16:12:57-05:00 | `242ff32` | Chris Tusa | Release v0.3.9: Fix GC-related crashes, use self for install subprocess |
+| 2026-03-08T23:40:56-05:00 | `c388de7` | Chris Tusa | Update CI to use Reef 0.5.2 |
+| 2026-03-08T23:38:23-05:00 | `2f04069` | Chris Tusa | Cache config paths to reduce TOML parsing allocation pressure |
+| 2026-03-08T20:05:35-05:00 | `604db08` | Chris Tusa | Fix O(n²) manifest generation using StringBuilder (BUG-027 workaround) |
+| 2026-03-08T15:37:57-05:00 | `b88a15a` | Chris Tusa | Fix infinite CPU loop in packaging step |
+| 2026-03-08T12:09:49-05:00 | `fbe0637` | Chris Tusa | Release v0.3.5: Wire up all unwired features and cleanup |
+| 2026-03-08T00:14:11-06:00 | `02417f9` | Chris Tusa | Recursive build-from-source dependency resolver |
+| 2026-03-07T23:10:59-06:00 | `cc4f8e3` | Chris Tusa | Export $PREFIX, $SYSCONFDIR, $PKG_ROOT to build scripts |
+| 2026-03-07T22:15:22-06:00 | `3f74bcd` | Chris Tusa | Auto-install missing build dependencies before compilation |
+| 2026-03-07T21:06:29-06:00 | `7842208` | Chris Tusa | Release v0.3.0: Package Format v2 complete |
+| 2026-03-07T20:40:41-06:00 | `d7c9548` | Chris Tusa | Rewrite script model: remove .reef type, use zsh, add upgrade flow |
+| 2026-03-07T20:06:41-06:00 | `261fd8e` | Chris Tusa | Add config file protection on package removal |
+| 2026-03-07T20:02:14-06:00 | `373d499` | Chris Tusa | Add [dependencies] to .PKGINFO and update TODO status |
+| 2026-03-07T19:58:29-06:00 | `5289616` | Chris Tusa | Replace rsync with manifest-driven fs.ops installer |
+| 2026-03-07T19:47:23-06:00 | `6662461` | Chris Tusa | Add mtree manifest module and replace .FOOTPRINT with .MANIFEST in build |
+| 2026-03-03T19:45:55-06:00 | `f220c00` | Chris Tusa | Release v0.2.11: Fix build script shell invocation and CPU detection |
+| 2026-03-03T12:12:18-06:00 | `c0d1b54` | Chris Tusa | Update CI to use reef-lang v0.3.4 |
+| 2026-03-03T11:55:01-06:00 | `8c6d1a8` | Chris Tusa | Release v0.2.10 |
+| 2026-03-02T23:16:32-06:00 | `601da8e` | Chris Tusa | Fix example config and default arch to match Zygaena defaults |
+| 2026-02-19T23:56:38-06:00 | `373f7f4` | Chris Tusa | Release v0.2.9: Replace shell workaround with str.join() for footprint processing |
+| 2026-02-19T23:47:07-06:00 | `6d0400d` | Chris Tusa | Release v0.2.8: Fix register hang by replacing string building with shell pipeline |
+| 2026-02-19T23:33:19-06:00 | `38023ac` | Chris Tusa | Release v0.2.7: Fix install hang on large packages (161K+ files) |
+| 2026-02-19T23:17:55-06:00 | `fae09e1` | Chris Tusa | Release v0.2.6: Fix files.db rebuild hang from massive over-allocation |
+| 2026-02-19T23:04:47-06:00 | `3ae110a` | Chris Tusa | Release v0.2.5: Fix files.db sort hang on large packages |
+| 2026-02-19T22:50:33-06:00 | `75d402d` | Chris Tusa | Update documentation for v0.2.4 features |
+| 2026-02-19T22:45:31-06:00 | `b416c5a` | Chris Tusa | Release v0.2.4: Fix resolver bug, dynamic categories, ports index, ports command |
+| 2026-02-19T20:40:02-06:00 | `2c66115` | Chris Tusa | Support category/name format in port find |
+| 2026-02-19T20:38:07-06:00 | `606628b` | Chris Tusa | Add hammerhead category to port search list |
+| 2026-01-30T18:43:31-06:00 | `623ec00` | Chris Tusa | Update woodpecker CI to use reef-lang-0.1.16 |
+| 2026-01-30T17:31:25-06:00 | `826d764` | Chris Tusa | Release v0.2.3: Add download progress bar and allow non-root builds |
+| 2026-01-30T10:21:47-06:00 | `9f1c025` | Chris Tusa | updated gitignore |
+| 2026-01-30T09:19:28-06:00 | `8d1f81d` | Chris Tusa | updated woodpecker to use reef-lang-0.1.15 |
+| 2026-01-29T22:20:13-06:00 | `a668688` | Chris Tusa | Release v0.2.2: Fix MsgPack parsing and add database documentation |
+| 2026-01-29T21:48:55-06:00 | `2e8a15f` | Chris Tusa | Implement MsgPack-based package database indexes |
+| 2026-01-29T21:01:07-06:00 | `9478e62` | Chris Tusa | Fix --root/--prefix flag parsing to work in any position |
+| 2026-01-29T20:38:25-06:00 | `12dcf11` | Chris Tusa | mkchroot: Generate scripts alongside chroot directory |
+| 2026-01-29T20:34:49-06:00 | `aac35d4` | Chris Tusa | Move chroot scripts to tools/ and add coral binary to gitignore |
+| 2026-01-29T20:23:20-06:00 | `6456fce` | Chris Tusa | Remove resolved bug reports and deprecated cache command |
+| 2026-01-29T20:22:08-06:00 | `11a24e2` | Chris Tusa | Release v0.2.1: Add --root support for alternate filesystem operations |
+| 2026-01-28T17:41:59-06:00 | `d418f8e` | Chris Tusa | Release v0.1.10: illumos compatibility and file tracking fixes |
+| 2026-01-28T16:28:26-06:00 | `e4efc01` | Chris Tusa | Release v0.1.9: Fix XZ extraction hang with Reef 0.1.14 |
+| 2026-01-28T15:38:39-06:00 | `998a2cc` | Chris Tusa | Update extraction bug: XZ files still broken in 0.1.7 |
+| 2026-01-28T14:05:47-06:00 | `c2a68d0` | Chris Tusa | Release v0.1.7: Bug fixes and new features |
+| 2026-01-28T11:33:02-06:00 | `e082408` | Chris Tusa | Add bug report for source extraction hang |
+| 2026-01-28T00:27:36-06:00 | `39a6f92` | Chris Tusa | Add bug report for HTTP redirect and version selection issues |
+| 2026-01-27T22:00:43-06:00 | `de2a679` | Chris Tusa | Release v0.1.5: Build logging and output ordering fix |
+| 2026-01-27T17:41:23-06:00 | `e062adc` | Chris Tusa | Release v0.1.4: Local sources, empty package validation |
+| 2026-01-27T17:28:16-06:00 | `2f44050` | Chris Tusa | Add local source support and empty package validation |
+| 2026-01-27T15:48:45-06:00 | `ba09e01` | Chris Tusa | Handle existing releases in CI pipeline |
+| 2026-01-27T15:45:15-06:00 | `295065f` | Chris Tusa | Update Reef compiler to 0.1.10 |
+| 2026-01-27T13:15:05-06:00 | `2d49d52` | Chris Tusa | Release v0.1.3: Documentation updates and source mirrors |
+| 2026-01-27T12:14:54-06:00 | `1af9101` | Chris Tusa | Implement source mirrors support (Option B) |
+| 2026-01-27T11:30:35-06:00 | `457b3cb` | Chris Tusa | Add source mirrors refactor plan (Option B) |
+| 2026-01-27T11:03:36-06:00 | `f3b3e30` | Chris Tusa | Fix dependency bug |
+| 2026-01-27T10:51:34-06:00 | `0318423` | Chris Tusa | Add build cleanup options and implementation |
+| 2026-01-26T23:04:38-06:00 | `017c401` | Chris Tusa | Add gnu and illumos categories to port search |
+| 2026-01-26T22:54:13-06:00 | `22599cf` | Chris Tusa | Bump version to 0.1.2 |
+| 2026-01-26T22:49:56-06:00 | `52d81e2` | Chris Tusa | Install build-essential for C headers |
+| 2026-01-26T22:48:54-06:00 | `5b95ae4` | Chris Tusa | Simplify Reef setup - just add bin to PATH |
+| 2026-01-26T21:41:02-06:00 | `79d691a` | Chris Tusa | Set REEF_HOME in environment block, add reef-runtime symlink and gcc |
+| 2026-01-26T21:40:04-06:00 | `0bf8a20` | Chris Tusa | Workaround: symlink stdlib to reef-stdlib (Reef packaging bug) |
+| 2026-01-26T21:39:03-06:00 | `537795e` | Chris Tusa | Debug: check stdlib contents in binary distribution |
+| 2026-01-26T21:37:48-06:00 | `3a465aa` | Chris Tusa | Set both REEF_HOME and REEF_STDLIB for reefc |
+| 2026-01-26T21:35:36-06:00 | `618ab8c` | Chris Tusa | Debug: extract without strip, show structure |
+| 2026-01-26T21:34:25-06:00 | `c646c2f` | Chris Tusa | Set both REEF_HOME and REEF_STDLIB |
+| 2026-01-26T21:33:39-06:00 | `ffbb779` | Chris Tusa | Use REEF_HOME instead of REEF_STDLIB |
+| 2026-01-26T21:32:20-06:00 | `f2ef479` | Chris Tusa | Set REEF_STDLIB environment variable for reefc |
+| 2026-01-26T21:31:13-06:00 | `6bf7480` | Chris Tusa | Switch to Ubuntu 24.04 for GLIBC 2.38+ compatibility |
+| 2026-01-26T21:30:02-06:00 | `1c191e9` | Chris Tusa | Fix reefc path and clean up debug statements |
+| 2026-01-26T21:28:44-06:00 | `2486d04` | Chris Tusa | Debug: show tarball structure |
+| 2026-01-26T21:27:44-06:00 | `6e928a4` | Chris Tusa | Quote curl command to fix YAML parsing |
+| 2026-01-26T21:27:00-06:00 | `3a1cbd8` | Chris Tusa | Fix variable escaping for Woodpecker preprocessing |
+| 2026-01-26T21:09:23-06:00 | `aaa7a4f` | Chris Tusa | Add test_secret to debug secret injection |
+| 2026-01-26T21:08:30-06:00 | `c9cb48f` | Chris Tusa | Revert to reef_token secret, clean up pipeline |
+| 2026-01-26T21:06:39-06:00 | `15fe579` | Chris Tusa | Try API_KEY as variable name |
+| 2026-01-26T21:03:30-06:00 | `3a97f20` | Chris Tusa | Use curly braces for variable expansion |
+| 2026-01-26T21:02:49-06:00 | `7471b00` | Chris Tusa | Fix YAML quoting for curl commands |
+| 2026-01-26T20:58:40-06:00 | `82460c8` | Chris Tusa | Fresh Woodpecker pipeline from scratch |
+| 2026-01-26T20:54:54-06:00 | `d971e66` | Chris Tusa | TEMPORARY: hardcode token to test pipeline |
+| 2026-01-26T20:53:25-06:00 | `72f3fd6` | Chris Tusa | Add test_secret to debug Woodpecker secrets |
+| 2026-01-26T20:52:17-06:00 | `279134f` | Chris Tusa | Update secret name to coral-releases |
+| 2026-01-26T20:43:27-06:00 | `d049473` | Chris Tusa | Add deeper token debug - show first 4 chars, write to file |
+| 2026-01-26T20:38:34-06:00 | `6678097` | Chris Tusa | Add API debug to test reef-lang access |
+| 2026-01-26T20:35:41-06:00 | `77b6a6d` | Chris Tusa | Revert to environment/from_secret syntax |
+| 2026-01-26T20:34:38-06:00 | `5c06999` | Chris Tusa | Try secrets array syntax with source/target |
+| 2026-01-26T20:32:59-06:00 | `ffd3831` | Chris Tusa | Trigger CI to test trusted security setting |
+| 2026-01-26T20:28:47-06:00 | `e06e651` | Chris Tusa | Add FORGEJO_TOKEN back to build step |
+| 2026-01-26T20:25:05-06:00 | `4bda1d3` | Chris Tusa | Try Woodpecker built-in CI_FORGE_TOKEN |
+| 2026-01-26T20:22:02-06:00 | `b460a2d` | Chris Tusa | Fix YAML syntax for environment secrets |
+| 2026-01-26T20:19:57-06:00 | `0b63243` | Chris Tusa | Debug secrets and try alternative environment syntax |
+| 2026-01-26T19:56:57-06:00 | `1bdcbc3` | Chris Tusa | Add authentication to Reef compiler download |
+| 2026-01-26T19:55:29-06:00 | `293b761` | Chris Tusa | Fix Woodpecker CI pipeline for v3.12.0 compatibility |
+| 2026-01-26T19:54:26-06:00 | `88e57e2` | Chris Tusa | Add Woodpecker CI pipeline for build and release |
+| 2026-01-26T18:29:13-06:00 | `1995845` | Chris Tusa | Add curl fallback for HTTP downloads on illumos |
+| 2026-01-26T18:17:04-06:00 | `22468ff` | Chris Tusa | Add rooted resolver for alternate root installation |
+| 2026-01-26T18:12:02-06:00 | `3acc88a` | Chris Tusa | Add --root and --prefix support for alternate root installation |
+| 2026-01-26T17:41:36-06:00 | `ce380ed` | Chris Tusa | Add user confirmation prompts and improve command workflows |
+| 2026-01-26T17:36:52-06:00 | `cc6f827` | Chris Tusa | Fix http.reef for Reef 0.1.7 compatibility |
+| 2026-01-26T22:54:49-06:00 | `4b81063` | Chris Tusa | Update http.reef for Reef 0.1.8 native downloads |
+| 2026-01-25T18:31:58-06:00 | `a9e5b80` | Chris Tusa | updated gitignore to exclude releases/ binary |
+| 2026-01-25T18:26:46-06:00 | `b6fcf6c` | Chris Tusa | Add release script |
+| 2026-01-25T18:21:56-06:00 | `b03ff66` | Chris Tusa | Initial commit: Coral package manager for Zygaena |
diff -r 000000000000 -r d0a96d73e9e1 examples/coral.conf
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/coral.conf	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,102 @@
+# Coral Package Manager Configuration
+# /etc/coral/coral.conf
+#
+# This is a sample configuration file showing all available options.
+# Copy to /etc/coral/coral.conf and modify as needed.
+
+[general]
+# Target architecture
+arch = "amd64"
+
+# Parallel build jobs (0 = auto-detect CPU count)
+jobs = 0
+
+# Ask for confirmation before operations
+confirm = true
+
+# Enable colored output
+color = true
+
+[paths]
+# Alternate root directory (empty = /, used for cross-building)
+root = ""
+
+# Installation prefix
+prefix = "/usr/local"
+
+# Ports tree location
+ports = "/usr/zports"
+
+# Built packages cache
+packages = "/usr/zports/pkgs/packages"
+
+# Downloaded sources cache
+sources = "/usr/zports/pkgs/sources"
+
+# Installed package database
+database = "/var/lib/coral/installed"
+
+# Ports index directory
+index = "/var/lib/coral/db"
+
+# Build work directory
+build = "/usr/zports/pkgs/build"
+
+[build]
+# Default C compiler flags
+cflags = "-O2 -pipe"
+
+# Default C++ compiler flags
+cxxflags = "-O2 -pipe"
+
+# Default make flags
+makeflags = ""
+
+# Strip binaries
+strip = true
+
+# Compress man pages
+compress_man = true
+
+# Fall back to building from source if binary not available
+fallback_to_source = true
+
+# Clean work directory after successful build
+clean_work = true
+
+# Clean package staging directory after successful build
+clean_pkg = true
+
+# Log build output to file (in work directory)
+logging = true
+
+# Hide subprocess output from terminal (still logged to file)
+quiet = false
+
+[network]
+# Download timeout in seconds
+timeout = 30
+
+# Number of retry attempts
+retries = 3
+
+# Use proxy (reads http_proxy/https_proxy env vars)
+use_proxy = false
+
+[sync]
+# Auto-refresh repository metadata
+auto_refresh = false
+
+[cache]
+# Keep downloaded packages after install
+keep_packages = true
+
+# Keep downloaded sources after build
+keep_sources = true
+
+[security]
+# Require signed repository manifests
+require_signed_repos = false
+
+# Require signed packages
+require_signed_packages = false
diff -r 000000000000 -r d0a96d73e9e1 examples/package.toml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/package.toml	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,105 @@
+# Coral Package Definition
+# /usr/ports/<category>/<name>/package.toml
+#
+# This is a sample package.toml showing all available options.
+# Only [package] section with name and version is required.
+
+[package]
+# Required: Package name (lowercase, alphanumeric, hyphens allowed)
+name = "example-package"
+
+# Required: Package version (semantic versioning recommended)
+version = "1.2.3"
+
+# Package release number (increment when rebuilding same version)
+release = 1
+
+# Short description of the package
+description = "An example package demonstrating all package.toml options"
+
+# Project homepage URL
+url = "https://example.com/example-package"
+
+# License identifier (SPDX recommended: MIT, BSD-2-Clause, GPL-3.0, etc.)
+license = "MIT"
+
+# Package maintainer
+maintainer = "Your Name <you@example.com>"
+
+# Target architecture (x86_64, aarch64, or "any" for arch-independent)
+arch = "x86_64"
+
+# Alternative package names this provides (for dependency resolution)
+# If another package depends on "editor", this package can satisfy it
+alternatives = ["editor", "text-editor"]
+
+# Packages this conflicts with (cannot be installed together)
+conflicts = ["other-editor"]
+
+[dependencies]
+# Packages required at runtime
+runtime = ["libfoo", "libbar"]
+
+# Packages required only for building
+build = ["gcc", "gmake", "pkg-config"]
+
+# Alternative dependencies: any one of the listed packages satisfies the dep
+# Format: "base_name:option1,option2,option3"
+# Example: depends on any database
+# runtime = ["database:postgresql,mysql,sqlite"]
+
+# =============================================================================
+# Source Files - New format with mirror support (recommended)
+# =============================================================================
+
+# Each [[source]] block defines a source file to download
+[[source]]
+# Output filename (what to save the file as)
+file = "example-package-1.2.3.tar.gz"
+
+# URLs to download from (tried in order until success)
+# Can mix remote URLs and local files (relative to port directory)
+urls = [
+    "https://example.com/releases/example-package-1.2.3.tar.gz",
+    "https://mirror.example.org/example-package-1.2.3.tar.gz",
+    "example-package-1.2.3.tar.gz"
+]
+
+# SHA256 checksum for verification
+# Use "SKIP" to skip verification (not recommended for production)
+checksum = "abc123def456789..."
+
+# Whether to extract the archive (default: true)
+# Set to false for files that shouldn't be extracted (patches, data files)
+extract = true
+
+# Multiple sources example:
+# [[source]]
+# file = "extra-data.tar.gz"
+# urls = ["https://example.com/extra-data.tar.gz"]
+# checksum = "789xyz..."
+# extract = false
+
+# =============================================================================
+# Legacy Source Format (deprecated, still supported)
+# =============================================================================
+# [sources]
+# urls = ["https://example.com/example-package-1.2.3.tar.gz"]
+# checksums = ["abc123def456789..."]
+
+[patches]
+# Patch files to apply (in order), relative to port directory
+files = [
+    "patches/fix-build.patch",
+    "patches/add-feature.patch"
+]
+
+# Strip level for patch command (-p flag)
+strip = 1
+
+[build]
+# Enable parallel builds
+parallel = true
+
+# Number of parallel jobs (0 = auto-detect, uses JOBS env var)
+jobs = 0
diff -r 000000000000 -r d0a96d73e9e1 reef.toml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/reef.toml	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,18 @@
+[package]
+name = "coral"
+version = "0.4.2"
+author = "Zygaena Team"
+description = "Package manager for Zygaena"
+license = "BSD-2-Clause"
+url = "https://github.com/zygaena/coral"
+
+[build]
+entry = "src/main.reef"
+output = "coral"
+output_dir = "build"
+source_dirs = ["src"]
+# Note: On illumos/Solaris, build with: reefc build -l socket -l nsl
+
+[docs]
+output = "docs"
+include_private = false
diff -r 000000000000 -r d0a96d73e9e1 scripts/make-release.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/make-release.sh	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,55 @@
+#!/bin/bash
+#
+# Create a source release tarball for Coral
+#
+# Usage: ./scripts/make-release.sh [version]
+#
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+CORAL_DIR="$(dirname "$SCRIPT_DIR")"
+cd "$CORAL_DIR"
+
+# Get version from reef.toml or argument
+if [[ -n "$1" ]]; then
+    VERSION="$1"
+else
+    VERSION=$(grep '^version' reef.toml | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
+fi
+
+if [[ -z "$VERSION" ]]; then
+    echo "Error: Could not determine version"
+    exit 1
+fi
+
+RELEASE_NAME="coral-${VERSION}-source"
+RELEASE_DIR="releases"
+TARBALL="${RELEASE_DIR}/${RELEASE_NAME}.tar.xz"
+
+echo "Creating release: $RELEASE_NAME"
+
+# Create releases directory
+mkdir -p "$RELEASE_DIR"
+
+# Create tarball excluding build artifacts and git
+tar --exclude='build/*' \
+    --exclude='.git' \
+    --exclude='releases' \
+    --exclude='*.o' \
+    --exclude='*.a' \
+    --exclude='docs/superpowers' \
+    --exclude='.DS_Store' \
+    --exclude='*.swp' \
+    --exclude='*~' \
+    --transform "s,^,${RELEASE_NAME}/," \
+    -cJf "$TARBALL" \
+    reef.toml README.md src/ docs/ test/
+
+echo "Created: $TARBALL"
+ls -la "$TARBALL"
+
+# Generate checksum
+sha256sum "$TARBALL" > "${TARBALL}.sha256"
+echo "Checksum: ${TARBALL}.sha256"
+cat "${TARBALL}.sha256"
diff -r 000000000000 -r d0a96d73e9e1 src/commands/build.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/build.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,1291 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: build.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Build command - compile packages from ports
+
+******************************************************************************/
+
+module commands.build
+
+import sys.args
+import sys.process
+import io.file
+import io.dir
+import io.path
+import io.console
+import core.str
+import core.port
+import core.config
+import core.database
+import core.resolver
+import util.version as ver
+import types
+import util.color
+import util.prompt
+import util.checksum
+import util.archive
+import util.mtree
+import util.http as uhttp
+import exitcodes as ec
+
+export
+    fn execute(opts: types.GlobalOptions): int
+end export
+
+// Build context
+type BuildContext = struct
+    port: types.Port
+    work_dir: string
+    pkg_dir: string
+    src_dir: string
+    cfg: config.Config
+    noclean: bool
+    force_clean: bool
+    log_file: string
+    logging: bool
+    quiet: bool
+end BuildContext
+
+// Load a port from path or find it by name
+fn load_or_find_port(port_path: string): types.PortResult
+    // Check if it's a valid port directory (must contain package.toml)
+    // This prevents confusion with binary files or other directories with same name
+    if dir.dir_exists(port_path)
+        let toml_path = path.join_path(port_path, "package.toml")
+        if file.fileExists(toml_path)
+            color.print_action("Loading port from: " + port_path)
+            return port.load(port_path)
+        end if
+    end if
+
+    // Not a valid port directory, search by name
+    color.print_action("Finding port: " + port_path)
+    return port.find(port_path)
+end load_or_find_port
+
+fn execute(opts: types.GlobalOptions): int
+    // Parse arguments
+    let argc = args.count()
+
+    // Port argument should be after the command
+    if argc <= opts.cmd_index + 1
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    // Parse flags
+    mut noclean = false
+    mut force_clean = false
+    mut quiet_flag = false
+    mut verbose_flag = false
+    mut install_flag = false
+    mut port_path = ""
+
+    mut i = opts.cmd_index + 1
+    while i < argc
+        let arg = args.get(i)
+        // Skip global flags with values
+        if arg == "--root" or arg == "--prefix"
+            i = i + 2
+            continue
+        end if
+        if arg == "--noclean" or arg == "-n"
+            noclean = true
+        elif arg == "--clean" or arg == "-c"
+            force_clean = true
+        elif arg == "--quiet" or arg == "-q"
+            quiet_flag = true
+        elif arg == "--verbose" or arg == "-v"
+            verbose_flag = true
+        elif arg == "--install"
+            install_flag = true
+        elif arg == "--no-install"
+            install_flag = false
+        elif not str.starts_with(arg, "-")
+            if str.length(port_path) == 0
+                port_path = arg
+            end if
+        end if
+        i = i + 1
+    end while
+
+    if str.length(port_path) == 0
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    // Handle "." for current directory
+    if port_path == "."
+        port_path = dir.current_dir()
+    end if
+
+    // Load configuration
+    let cfg = config.load()
+    uhttp.enable_proxy(config.get_use_proxy(cfg))
+
+    // Try to find the port by name if path doesn't exist
+    let result = load_or_find_port(port_path)
+
+    if not result.success
+        color.print_error(result.error)
+        return ec.EXIT_PKG_NOT_FOUND()
+    end if
+
+    let p = result.port
+    color.print_success("Loaded port: " + p.info.name + " " + p.info.version)
+
+    // Dry-run: show build plan without executing
+    if opts.dry_run
+        color.print_info("Dry run — would build: " + p.info.name + " " + p.info.version)
+        let dry_resolve = resolver.resolve_build_deps(p.info.name)
+        if dry_resolve.success and dry_resolve.count > 1
+            color.print_info("Dependencies to build first:")
+            mut di = 0
+            while di < dry_resolve.count - 1
+                let dep_port = port.find(dry_resolve.packages[di])
+                if dep_port.success
+                    color.print_info("  " + dry_resolve.packages[di] + " " + dep_port.port.info.version)
+                else
+                    color.print_info("  " + dry_resolve.packages[di])
+                end if
+                di = di + 1
+            end while
+        end if
+        color.print_info("Dry run — no changes made")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Resolve, build, and install missing dependencies before building
+    if not resolve_and_build_deps(p, opts, cfg)
+        // Check if the failure was due to circular dependencies
+        let check_result = resolver.resolve_build_deps(p.info.name)
+        if not check_result.success and str.starts_with(check_result.error, "Circular")
+            color.print_error("Cannot build " + p.info.name + " — circular dependency")
+            return ec.EXIT_DEPS_CIRCULAR()
+        end if
+        color.print_error("Cannot build " + p.info.name + " — missing dependencies")
+        return ec.EXIT_DEPS_UNMET()
+    end if
+
+    // Determine quiet mode: CLI flag overrides config, verbose overrides quiet
+    mut quiet_mode = config.get_quiet(cfg)
+    if quiet_flag
+        quiet_mode = true
+    end if
+    if verbose_flag
+        quiet_mode = false
+    end if
+
+    // Create build context
+    let ctx = create_context(p, cfg, noclean, force_clean, quiet_mode)
+
+    // Step 1: Setup directories
+    if not setup_directories(ctx)
+        color.print_error("Failed to create build directories")
+        return ec.EXIT_ERROR()
+    end if
+
+    // Initialize logging after directories are created
+    init_log(ctx)
+    if ctx.logging and str.length(ctx.log_file) > 0
+        log_info(ctx, "Log file: " + ctx.log_file)
+    end if
+
+    // Step 2: Download sources
+    if not download_sources(ctx)
+        log_error(ctx, "Failed to download sources")
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return ec.EXIT_DOWNLOAD_FAILED()
+    end if
+
+    // Step 3: Verify checksums
+    if not verify_checksums(ctx)
+        log_error(ctx, "Checksum verification failed")
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return ec.EXIT_CHECKSUM_FAILED()
+    end if
+
+    // Step 4: Extract sources
+    if not extract_sources(ctx)
+        log_error(ctx, "Failed to extract sources")
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return ec.EXIT_EXTRACT_FAILED()
+    end if
+
+    // Step 5: Apply patches
+    if not apply_patches(ctx)
+        log_error(ctx, "Failed to apply patches")
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return ec.EXIT_PATCH_FAILED()
+    end if
+
+    // Step 6: Run build script
+    if not run_build(ctx)
+        log_error(ctx, "Build failed")
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return ec.EXIT_BUILD_FAILED()
+    end if
+
+    // Step 7: Compress man pages (if enabled)
+    if config.get_compress_man(ctx.cfg)
+        compress_man_pages(ctx)
+    end if
+
+    // Step 8: Create package
+    if not create_package(ctx)
+        log_error(ctx, "Failed to create package")
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return ec.EXIT_PACKAGE_FAILED()
+    end if
+
+    // Success!
+    let pkg_name = p.info.name + "-" + p.info.version + "-" + int_to_str(p.info.release) + ".pkg.tar.xz"
+    let pkg_path = path.join_path(config.get_packages_dir(cfg), pkg_name)
+    log_success(ctx, "Package created: " + pkg_path)
+
+    // Cleanup work directory on success
+    cleanup_build(ctx)
+
+    // Step 9: Install the built package (only with --install)
+    if install_flag
+        color.print_action("Installing " + p.info.name + "...")
+        let self_exe = args.get(0)
+        let install_cmd = self_exe + " install -y -f \"" + pkg_path + "\""
+        let install_pid = process.process_spawn_shell(install_cmd)
+        if install_pid < 0
+            color.print_error("Failed to launch installer for " + p.info.name)
+            return ec.EXIT_INSTALL_FAILED()
+        end if
+        let install_exit = process.process_wait(install_pid)
+        if install_exit != 0
+            color.print_error("Failed to install " + p.info.name)
+            return ec.EXIT_INSTALL_FAILED()
+        end if
+    else
+        color.print_success("Package ready (use --install to install, or: coral install \"" + pkg_path + "\")")
+    end if
+
+    return ec.EXIT_SUCCESS()
+end execute
+
+proc print_usage()
+    println("Usage: coral build [options] <port-path>")
+    println("")
+    println("Build a package from a port directory.")
+    println("")
+    println("Arguments:")
+    println("  <port-path>    Path to port directory (use . for current)")
+    println("")
+    println("Options:")
+    println("  --install      Install the package after building")
+    println("  --no-install   Build only, do not install (default)")
+    println("  -n, --noclean  Don't clean work/pkg directories after build")
+    println("  -c, --clean    Force cleanup even if disabled in config")
+    println("  -q, --quiet    Hide subprocess output (still logged to file)")
+    println("  -v, --verbose  Show all output (overrides quiet)")
+    println("")
+    println("Examples:")
+    println("  coral build /usr/zports/base/vim")
+    println("  coral build --install base/vim")
+    println("  coral build --noclean .")
+    println("  coral build --quiet vim")
+end print_usage
+
+// Resolve all build dependencies recursively, build from source, and install each.
+// Returns true if all dependencies are satisfied (either already installed or built+installed).
+fn resolve_and_build_deps(p: types.Port, opts: types.GlobalOptions, cfg: config.Config): bool
+    // Resolve the full dependency tree (runtime + build deps, recursive)
+    let resolve_result = resolver.resolve_build_deps(p.info.name)
+
+    if not resolve_result.success
+        color.print_error("Dependency resolution failed: " + resolve_result.error)
+        return false
+    end if
+
+    // The result includes the target package as the last element — strip it
+    mut dep_count = resolve_result.count
+    if dep_count > 0
+        let last = resolve_result.packages[dep_count - 1]
+        if str.equals(last, p.info.name)
+            dep_count = dep_count - 1
+        end if
+    end if
+
+    // No deps to build
+    if dep_count == 0
+        return true
+    end if
+
+    // Look up version info for each dep to display in the build plan
+    mut dep_versions: [string] = new [string](256)
+    mut i = 0
+    while i < dep_count
+        let dep_port_result = port.find(resolve_result.packages[i])
+        if dep_port_result.success
+            dep_versions[i] = dep_port_result.port.info.version
+        else
+            dep_versions[i] = ""
+        end if
+        i = i + 1
+    end while
+
+    // Show build plan and ask for confirmation
+    if not prompt.confirm_build_plan(resolve_result.packages, dep_versions, dep_count, p.info.name, opts)
+        color.print_info("Build cancelled")
+        return false
+    end if
+
+    // Build and install each dependency in topological order
+    i = 0
+    while i < dep_count
+        let dep_name = resolve_result.packages[i]
+        println("")
+        color.print_action("[" + int_to_str(i + 1) + "/" + int_to_str(dep_count) + "] Building dependency: " + dep_name)
+
+        // Build the dependency from source
+        if not build_single_port(dep_name, cfg, opts)
+            color.print_error("Failed to build dependency: " + dep_name)
+            return false
+        end if
+
+        // Install the built package
+        let dep_port_result = port.find(dep_name)
+        if not dep_port_result.success
+            color.print_error("Cannot find port after build: " + dep_name)
+            return false
+        end if
+
+        let dp = dep_port_result.port
+        let pkg_filename = dp.info.name + "-" + dp.info.version + "-" + int_to_str(dp.info.release) + ".pkg.tar.xz"
+        let pkg_path = path.join_path(config.get_packages_dir(cfg), pkg_filename)
+
+        if not file.fileExists(pkg_path)
+            color.print_error("Built package not found: " + pkg_path)
+            return false
+        end if
+
+        color.print_action("Installing " + dep_name + "...")
+        let self_exe = args.get(0)
+        let install_cmd = self_exe + " install -y \"" + pkg_path + "\""
+        let pid = process.process_spawn_shell(install_cmd)
+        if pid < 0
+            color.print_error("Failed to launch installer for " + dep_name)
+            return false
+        end if
+        let exit_code = process.process_wait(pid)
+        if exit_code != 0
+            color.print_error("Failed to install " + dep_name)
+            return false
+        end if
+
+        color.print_success("Dependency ready: " + dep_name)
+        i = i + 1
+    end while
+
+    println("")
+    color.print_success("All " + int_to_str(dep_count) + " dependencies built and installed")
+    return true
+end resolve_and_build_deps
+
+// Build a single port from source. Used for dependency builds.
+// Returns true if build succeeds and package archive is created.
+fn build_single_port(name: string, cfg: config.Config, opts: types.GlobalOptions): bool
+    let port_result = port.find(name)
+    if not port_result.success
+        color.print_error("Port not found: " + name)
+        return false
+    end if
+
+    let p = port_result.port
+
+    // Determine quiet mode
+    mut quiet_mode = config.get_quiet(cfg)
+    if opts.verbose
+        quiet_mode = false
+    end if
+    if opts.quiet
+        quiet_mode = true
+    end if
+
+    let ctx = create_context(p, cfg, false, false, quiet_mode)
+
+    // Run the full build pipeline
+    if not setup_directories(ctx)
+        color.print_error("Failed to create build directories for " + name)
+        return false
+    end if
+
+    init_log(ctx)
+
+    if not download_sources(ctx)
+        log_error(ctx, "Failed to download sources for " + name)
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return false
+    end if
+
+    if not verify_checksums(ctx)
+        log_error(ctx, "Checksum verification failed for " + name)
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return false
+    end if
+
+    if not extract_sources(ctx)
+        log_error(ctx, "Failed to extract sources for " + name)
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return false
+    end if
+
+    if not apply_patches(ctx)
+        log_error(ctx, "Failed to apply patches for " + name)
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return false
+    end if
+
+    if not run_build(ctx)
+        log_error(ctx, "Build failed for " + name)
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return false
+    end if
+
+    // Compress man pages (if enabled)
+    if config.get_compress_man(cfg)
+        compress_man_pages(ctx)
+    end if
+
+    if not create_package(ctx)
+        log_error(ctx, "Failed to create package for " + name)
+        print_log_location(ctx)
+        cleanup_build(ctx)
+        return false
+    end if
+
+    let pkg_filename = p.info.name + "-" + p.info.version + "-" + int_to_str(p.info.release) + ".pkg.tar.xz"
+    let pkg_path = path.join_path(config.get_packages_dir(cfg), pkg_filename)
+    log_success(ctx, "Package created: " + pkg_path)
+
+    cleanup_build(ctx)
+    return true
+end build_single_port
+
+fn create_context(p: types.Port, cfg: config.Config, noclean: bool, force_clean: bool, quiet: bool): BuildContext
+    // New directory structure:
+    //   /usr/zports/pkgs/build/<pkgname>/src/   - extracted sources, compilation
+    //   /usr/zports/pkgs/build/<pkgname>/stage/ - DESTDIR for make install
+    let build_base = config.get_build_dir(cfg)
+    let pkg_build_dir = path.join_path(build_base, p.info.name)
+    let work_dir = path.join_path(pkg_build_dir, "src")
+    let pkg_dir = path.join_path(pkg_build_dir, "stage")
+
+    // Generate log file path with timestamp (in package build root, not src/)
+    let logging_enabled = config.get_logging(cfg)
+    mut log_path = ""
+    if logging_enabled
+        log_path = path.join_path(pkg_build_dir, "coral-build-" + p.info.name + "-" + get_timestamp() + ".log")
+    end if
+
+    return BuildContext{
+        port: p,
+        work_dir: work_dir,
+        pkg_dir: pkg_dir,
+        src_dir: "",
+        cfg: cfg,
+        noclean: noclean,
+        force_clean: force_clean,
+        log_file: log_path,
+        logging: logging_enabled,
+        quiet: quiet
+    }
+end create_context
+
+// Get current timestamp for log filename
+fn get_timestamp(): string
+    // Use date command to get timestamp
+    let tmp_file = "/tmp/coral_ts_" + int_to_str(process.getpid()) + ".txt"
+    let cmd = "date '+%Y%m%d-%H%M%S' > \"" + tmp_file + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid > 0
+        process.process_wait(pid)
+    end if
+
+    let ts = file.readFile(tmp_file)
+
+    // Cleanup
+    let rm_cmd = "rm -f \"" + tmp_file + "\""
+    let rm_pid = process.process_spawn_shell(rm_cmd)
+    if rm_pid > 0
+        process.process_wait(rm_pid)
+    end if
+
+    return str.trim(ts, '\n')
+end get_timestamp
+
+// Initialize log file with header
+proc init_log(ctx: BuildContext)
+    if not ctx.logging
+        return
+    end if
+
+    mut header = "=== Coral Build Log ===\n"
+    header = header + "Package: " + ctx.port.info.name + "\n"
+    header = header + "Version: " + ctx.port.info.version + "\n"
+    header = header + "Work Dir: " + ctx.work_dir + "\n"
+    header = header + "Log File: " + ctx.log_file + "\n"
+    header = header + "========================\n\n"
+
+    file.writeFile(ctx.log_file, header)
+end init_log
+
+// Log a message to file and optionally to screen
+proc log_msg(ctx: BuildContext, msg: string, show_screen: bool)
+    // Always write to log file if logging enabled
+    if ctx.logging and str.length(ctx.log_file) > 0
+        file.appendFile(ctx.log_file, msg + "\n")
+    end if
+
+    // Show on screen unless quiet mode suppresses it
+    if show_screen
+        println(msg)
+    end if
+end log_msg
+
+// Log info message (always show on screen)
+proc log_info(ctx: BuildContext, msg: string)
+    if ctx.logging and str.length(ctx.log_file) > 0
+        file.appendFile(ctx.log_file, "[INFO] " + msg + "\n")
+    end if
+    color.print_info(msg)
+end log_info
+
+// Log success message (always show on screen)
+proc log_success(ctx: BuildContext, msg: string)
+    if ctx.logging and str.length(ctx.log_file) > 0
+        file.appendFile(ctx.log_file, "[OK] " + msg + "\n")
+    end if
+    color.print_success(msg)
+end log_success
+
+// Log error message (always show on screen)
+proc log_error(ctx: BuildContext, msg: string)
+    if ctx.logging and str.length(ctx.log_file) > 0
+        file.appendFile(ctx.log_file, "[ERROR] " + msg + "\n")
+    end if
+    color.print_error(msg)
+end log_error
+
+// Log action message (always show on screen)
+proc log_action(ctx: BuildContext, msg: string)
+    if ctx.logging and str.length(ctx.log_file) > 0
+        file.appendFile(ctx.log_file, "[ACTION] " + msg + "\n")
+    end if
+    color.print_action(msg)
+end log_action
+
+// Log warning message (always show on screen)
+proc log_warning(ctx: BuildContext, msg: string)
+    if ctx.logging and str.length(ctx.log_file) > 0
+        file.appendFile(ctx.log_file, "[WARN] " + msg + "\n")
+    end if
+    color.print_warning(msg)
+end log_warning
+
+// Print log file location (for debugging failed builds)
+proc print_log_location(ctx: BuildContext)
+    if ctx.logging and str.length(ctx.log_file) > 0
+        color.print_info("Build log: " + ctx.log_file)
+    end if
+end print_log_location
+
+// Run a shell command with output captured to log
+// Returns exit code
+fn run_logged_cmd(ctx: BuildContext, cmd: string): int
+    if ctx.logging and str.length(ctx.log_file) > 0
+        file.appendFile(ctx.log_file, "[CMD] " + cmd + "\n")
+    end if
+
+    // Build command that logs output to file
+    // Redirects all output to log file to avoid tee pipeline issues
+    // (tee caused SIGPIPE on long builds and masked exit codes)
+    mut full_cmd = cmd
+    if ctx.logging and str.length(ctx.log_file) > 0
+        // Both quiet and normal mode: log to file only
+        // Real-time terminal output is sacrificed for build reliability
+        full_cmd = "(" + cmd + ") >> \"" + ctx.log_file + "\" 2>&1"
+    else
+        if ctx.quiet
+            // No logging, quiet mode: suppress output
+            full_cmd = "(" + cmd + ") >/dev/null 2>&1"
+        end if
+        // No logging, not quiet: just run normally
+    end if
+
+    // Flush stdout before spawning subprocess to ensure proper output ordering
+    console.flushOutput()
+
+    let pid = process.process_spawn_shell(full_cmd)
+    if pid < 0
+        return -1
+    end if
+
+    return process.process_wait(pid)
+end run_logged_cmd
+
+fn setup_directories(ctx: BuildContext): bool
+    color.print_info("Setting up build directories...")
+
+    // Create work directory
+    if dir.dir_exists(ctx.work_dir)
+        // Clean existing work dir
+        let cmd = "rm -rf \"" + ctx.work_dir + "\""
+        let pid = process.process_spawn_shell(cmd)
+        if pid > 0
+            process.process_wait(pid)
+        end if
+    end if
+
+    if not dir.create_dir_all(ctx.work_dir)
+        return false
+    end if
+
+    // Create pkg directory (for DESTDIR)
+    if dir.dir_exists(ctx.pkg_dir)
+        let cmd = "rm -rf \"" + ctx.pkg_dir + "\""
+        let pid = process.process_spawn_shell(cmd)
+        if pid > 0
+            process.process_wait(pid)
+        end if
+    end if
+
+    if not dir.create_dir_all(ctx.pkg_dir)
+        return false
+    end if
+
+    // Ensure sources directory exists
+    let sources_dir = config.get_sources_dir(ctx.cfg)
+    if not dir.dir_exists(sources_dir)
+        if not dir.create_dir_all(sources_dir)
+            return false
+        end if
+    end if
+
+    // Ensure packages directory exists
+    let packages_dir = config.get_packages_dir(ctx.cfg)
+    if not dir.dir_exists(packages_dir)
+        if not dir.create_dir_all(packages_dir)
+            return false
+        end if
+    end if
+
+    return true
+end setup_directories
+
+fn download_sources(ctx: BuildContext): bool
+    if ctx.port.sources.count == 0
+        color.print_info("No sources to download")
+        return true
+    end if
+
+    color.print_info("Downloading sources...")
+
+    let sources_dir = config.get_sources_dir(ctx.cfg)
+
+    mut i = 0
+    while i < ctx.port.sources.count
+        let src = ctx.port.sources.sources[i]
+        let filename = src.file
+        let dest = path.join_path(sources_dir, filename)
+
+        // Check if already cached with valid checksum
+        if file.fileExists(dest)
+            if verify_single_checksum(dest, src.checksum)
+                color.print_info("Using cached: " + filename)
+                i = i + 1
+                continue
+            else
+                // Cached file has wrong checksum, re-download
+                color.print_warning("Cached file checksum mismatch, re-downloading: " + filename)
+            end if
+        end if
+
+        // Try each mirror URL in order
+        mut download_success = false
+        mut url_idx = 0
+        while url_idx < src.url_count
+            let url = src.urls[url_idx]
+
+            if url_idx > 0
+                color.print_info("Trying mirror: " + url)
+            end if
+
+            // Check if this is a local file (no http:// or https:// prefix)
+            if is_local_source(url)
+                // Local file - copy from port directory to sources cache
+                if copy_local_source(url, dest, ctx.port.port_dir)
+                    if verify_single_checksum(dest, src.checksum)
+                        download_success = true
+                        break
+                    else
+                        color.print_warning("Checksum mismatch for local file, trying next...")
+                        let rm_cmd = "rm -f \"" + dest + "\""
+                        let rm_pid = process.process_spawn_shell(rm_cmd)
+                        if rm_pid > 0
+                            process.process_wait(rm_pid)
+                        end if
+                    end if
+                end if
+            else
+                // Remote URL - download via HTTP
+                if uhttp.download(url, dest)
+                    // Verify checksum after download
+                    if verify_single_checksum(dest, src.checksum)
+                        download_success = true
+                        break
+                    else
+                        color.print_warning("Checksum mismatch from mirror, trying next...")
+                        // Delete bad file
+                        let rm_cmd = "rm -f \"" + dest + "\""
+                        let rm_pid = process.process_spawn_shell(rm_cmd)
+                        if rm_pid > 0
+                            process.process_wait(rm_pid)
+                        end if
+                    end if
+                end if
+            end if
+
+            url_idx = url_idx + 1
+        end while
+
+        if not download_success
+            color.print_error("Failed to download: " + filename + " (all sources failed)")
+            return false
+        end if
+
+        i = i + 1
+    end while
+
+    return true
+end download_sources
+
+// Check if a URL is a local file path (not http:// or https://)
+fn is_local_source(url: string): bool
+    if str.starts_with(url, "http://")
+        return false
+    end if
+    if str.starts_with(url, "https://")
+        return false
+    end if
+    return true
+end is_local_source
+
+// Copy a local source file from port directory to sources cache
+fn copy_local_source(source_path: string, dest: string, port_dir: string): bool
+    // Resolve the source path
+    mut full_path = source_path
+
+    // If it's a relative path, resolve relative to port directory
+    if not str.starts_with(source_path, "/")
+        full_path = path.join_path(port_dir, source_path)
+    end if
+
+    // Check if file exists
+    if not file.fileExists(full_path)
+        color.print_error("Local source file not found: " + full_path)
+        return false
+    end if
+
+    color.print_info("Copying local source: " + source_path)
+
+    // Copy the file
+    let cmd = "cp \"" + full_path + "\" \"" + dest + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return false
+    end if
+
+    let exit_code = process.process_wait(pid)
+    if exit_code != 0
+        color.print_error("Failed to copy local source: " + source_path)
+        return false
+    end if
+
+    color.print_success("Copied: " + source_path)
+    return true
+end copy_local_source
+
+// Verify a single file's checksum
+fn verify_single_checksum(file_path: string, expected: string): bool
+    // Handle special checksum values
+    if str.equals(expected, "SKIP") or str.equals(expected, "BOOTSTRAP") or str.equals(expected, "skip")
+        return true
+    end if
+
+    if str.length(expected) == 0
+        return true
+    end if
+
+    return checksum.verify_checksum_with_algo(file_path, expected)
+end verify_single_checksum
+
+fn verify_checksums(ctx: BuildContext): bool
+    if ctx.port.sources.count == 0
+        return true
+    end if
+
+    color.print_info("Verifying checksums...")
+
+    let sources_dir = config.get_sources_dir(ctx.cfg)
+
+    mut i = 0
+    while i < ctx.port.sources.count
+        let src = ctx.port.sources.sources[i]
+        let filename = src.file
+        let file_path = path.join_path(sources_dir, filename)
+        let expected = src.checksum
+
+        // Handle special checksum values
+        if str.equals(expected, "SKIP") or str.equals(expected, "BOOTSTRAP") or str.equals(expected, "skip")
+            color.print_warning("Checksum verification skipped: " + filename)
+        elif str.length(expected) > 0
+            if not checksum.verify_checksum_with_algo(file_path, expected)
+                color.print_error("Checksum mismatch: " + filename)
+                return false
+            end if
+            color.print_success("Checksum verified: " + filename)
+        else
+            color.print_warning("No checksum for: " + filename)
+        end if
+
+        i = i + 1
+    end while
+
+    return true
+end verify_checksums
+
+fn extract_sources(ctx: BuildContext): bool
+    if ctx.port.sources.count == 0
+        return true
+    end if
+
+    color.print_info("Extracting sources...")
+
+    let sources_dir = config.get_sources_dir(ctx.cfg)
+
+    mut i = 0
+    while i < ctx.port.sources.count
+        let src = ctx.port.sources.sources[i]
+        let filename = src.file
+        let archive_path = path.join_path(sources_dir, filename)
+
+        // Check extract flag
+        if not src.extract
+            // Copy file to work_dir instead of extracting (for patches, etc.)
+            let dest_path = path.join_path(ctx.work_dir, filename)
+            let cp_cmd = "cp \"" + archive_path + "\" \"" + dest_path + "\""
+            let cp_pid = process.process_spawn_shell(cp_cmd)
+            if cp_pid > 0
+                let exit_code = process.process_wait(cp_pid)
+                if exit_code != 0
+                    color.print_error("Failed to copy: " + filename)
+                    return false
+                end if
+            end if
+            color.print_info("Copied (no extract): " + filename)
+            i = i + 1
+            continue
+        end if
+
+        if not archive.extract(archive_path, ctx.work_dir)
+            color.print_error("Failed to extract: " + filename)
+            return false
+        end if
+
+        color.print_success("Extracted: " + filename)
+        i = i + 1
+    end while
+
+    return true
+end extract_sources
+
+fn apply_patches(ctx: BuildContext): bool
+    if ctx.port.patches.count == 0
+        return true
+    end if
+
+    log_info(ctx, "Applying patches...")
+
+    let strip = ctx.port.patches.strip
+
+    // Find the source directory (first subdirectory in work_dir)
+    let find_srcdir = "cd \"" + ctx.work_dir + "\" && ls -d */ 2>/dev/null | head -1 | tr -d '/'"
+
+    mut i = 0
+    while i < ctx.port.patches.count
+        let patch_file = ctx.port.patches.files[i]
+        let patch_path = path.join_path(ctx.port.port_dir, patch_file)
+
+        if not file.fileExists(patch_path)
+            log_error(ctx, "Patch file not found: " + patch_file)
+            return false
+        end if
+
+        // Apply patch from within the source directory
+        let cmd = "cd \"" + ctx.work_dir + "/$(" + find_srcdir + ")\" && patch -p" + int_to_str(strip) + " < \"" + patch_path + "\""
+        let exit_code = run_logged_cmd(ctx, cmd)
+        if exit_code != 0
+            log_error(ctx, "Failed to apply patch: " + patch_file)
+            return false
+        end if
+
+        log_success(ctx, "Applied patch: " + patch_file)
+        i = i + 1
+    end while
+
+    return true
+end apply_patches
+
+fn run_build(ctx: BuildContext): bool
+    log_action(ctx, "Building " + ctx.port.info.name + "...")
+
+    // Look for build.sh in port directory
+    let build_script = path.join_path(ctx.port.port_dir, "build.sh")
+
+    if not file.fileExists(build_script)
+        log_error(ctx, "No build.sh found in port directory")
+        return false
+    end if
+
+    // Set up environment
+    let destdir = ctx.pkg_dir
+    let cflags = config.get_cflags(ctx.cfg)
+    let cxxflags = config.get_cxxflags(ctx.cfg)
+    let jobs = config.get_jobs(ctx.cfg)
+
+    mut jobs_str = ""
+    if jobs > 0
+        jobs_str = int_to_str(jobs)
+    else
+        // Auto-detect: use getconf (portable across illumos/Linux, unlike nproc)
+        jobs_str = "$(getconf NPROCESSORS_ONLN 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)"
+    end if
+
+    // Find the extracted source directory (first directory in work_dir)
+    // Typically the tarball extracts to a subdirectory like "zlib-1.3.1"
+    let find_srcdir_cmd = "cd \"" + ctx.work_dir + "\" && ls -d */ 2>/dev/null | head -1 | tr -d '/'"
+
+    // Determine PREFIX: package.toml override → coral.conf → default (/usr)
+    mut prefix = config.get_prefix(ctx.cfg)
+    if str.length(ctx.port.prefix) > 0
+        prefix = ctx.port.prefix
+    end if
+
+    // Determine SYSCONFDIR: package.toml override → coral.conf → default (/etc)
+    mut sysconfdir = config.get_sysconfdir(ctx.cfg)
+    if str.length(ctx.port.sysconfdir) > 0
+        sysconfdir = ctx.port.sysconfdir
+    end if
+
+    // PKG_ROOT is the alternate root (empty means /)
+    let pkg_root = config.get_root(ctx.cfg)
+
+    // Build the command with all environment variables
+    // Per Hammerhead porting guide: no uname wrappers, replace bundled config.guess/config.sub,
+    // use system config.guess for BUILD_TRIPLE (returns x86_64-zygaena-hammerhead)
+    mut cmd = "cd \"" + ctx.work_dir + "\" && "
+    cmd = cmd + "export SRCDIR=\"" + ctx.work_dir + "/$(" + find_srcdir_cmd + ")\" && "
+    cmd = cmd + "export PKGDIR=\"" + destdir + "\" && "
+    cmd = cmd + "export DESTDIR=\"" + destdir + "\" && "
+    cmd = cmd + "export PREFIX=\"" + prefix + "\" && "
+    cmd = cmd + "export SYSCONFDIR=\"" + sysconfdir + "\" && "
+    cmd = cmd + "export PKG_ROOT=\"" + pkg_root + "\" && "
+    cmd = cmd + "export JOBS=\"" + jobs_str + "\" && "
+    cmd = cmd + "export CFLAGS=\"" + cflags + "\" && "
+    cmd = cmd + "export CXXFLAGS=\"" + cxxflags + "\" && "
+    cmd = cmd + "export LDFLAGS=\"-R" + prefix + "/lib\" && "
+    cmd = cmd + "export PKG_CONFIG_PATH=\"" + prefix + "/lib/pkgconfig\" && "
+    cmd = cmd + "export MAKEFLAGS=\"-j" + jobs_str + "\" && "
+    cmd = cmd + "export PKG_NAME=\"" + ctx.port.info.name + "\" && "
+    cmd = cmd + "export PKG_VERSION=\"" + ctx.port.info.version + "\" && "
+    cmd = cmd + "export BUILD_TRIPLE=\"$(/usr/share/autoconf/build-aux/config.guess 2>/dev/null || echo x86_64-zygaena-hammerhead)\" && "
+    cmd = cmd + "export PATH=\"/usr/gnu/bin:$PATH\" && "
+    cmd = cmd + "export MAKE=gmake && "
+    cmd = cmd + "export TAR=gtar && "
+    cmd = cmd + "for _f in config.guess config.sub; do find \"$SRCDIR\" -name \"$_f\" -exec cp /usr/share/autoconf/build-aux/\"$_f\" {} \\; 2>/dev/null; done; "
+    cmd = cmd + "zsh \"" + build_script + "\""
+
+    log_info(ctx, "Running build script...")
+
+    let exit_code = run_logged_cmd(ctx, cmd)
+    if exit_code != 0
+        log_error(ctx, "Build script failed with exit code: " + int_to_str(exit_code))
+        return false
+    end if
+
+    log_success(ctx, "Build completed successfully")
+    return true
+end run_build
+
+// Compress uncompressed man pages in the staging directory
+proc compress_man_pages(ctx: BuildContext)
+    // Find uncompressed man pages and gzip them
+    // Uses -exec {} + (batch mode) to avoid shell escaping issues with \;
+    // Uses -type f to skip symlinks (avoids creating dangling refs)
+    let script = "find '" + ctx.pkg_dir + "' -type f -path '*/man/man*/*' ! -name '*.gz' ! -name '*.bz2' ! -name '*.xz' -exec gzip -9 {} +"
+
+    let pid = process.process_spawn_shell(script)
+    if pid > 0
+        let exit_code = process.process_wait(pid)
+        if exit_code == 0
+            log_info(ctx, "Compressed man pages")
+        end if
+    end if
+end compress_man_pages
+
+fn create_package(ctx: BuildContext): bool
+    color.print_action("Creating package...")
+
+    // Validate that staging directory has content (not just metadata files)
+    if not validate_staging_dir(ctx.pkg_dir)
+        color.print_error("Staging directory is empty - build produced no files")
+        color.print_error("Check that build.sh installs files to $DESTDIR or $PKGDIR")
+        return false
+    end if
+
+    // Generate .PKGINFO
+    if not generate_pkginfo(ctx)
+        return false
+    end if
+
+    // Generate .MANIFEST (mtree file inventory)
+    if not generate_manifest_file(ctx)
+        return false
+    end if
+
+    // Create the package archive
+    let pkg_name = ctx.port.info.name + "-" + ctx.port.info.version + "-" + int_to_str(ctx.port.info.release) + ".pkg.tar.xz"
+    let pkg_path = path.join_path(config.get_packages_dir(ctx.cfg), pkg_name)
+
+    if not archive.create_package(ctx.pkg_dir, pkg_path)
+        color.print_error("Failed to create package archive")
+        return false
+    end if
+
+    return true
+end create_package
+
+// Validate that staging directory has actual content (not empty)
+fn validate_staging_dir(pkg_dir: string): bool
+    // Count files in staging directory, excluding metadata files
+    // A valid package should have at least one real file
+    let cmd = "find \"" + pkg_dir + "\" -type f ! -name '.PKGINFO' ! -name '.MANIFEST' ! -name '.FOOTPRINT' | head -1"
+    let tmp_file = "/tmp/coral_validate_" + int_to_str(process.getpid()) + ".txt"
+    let full_cmd = cmd + " > \"" + tmp_file + "\""
+
+    let pid = process.process_spawn_shell(full_cmd)
+    if pid < 0
+        return false
+    end if
+
+    process.process_wait(pid)
+
+    // Read result
+    let content = file.readFile(tmp_file)
+
+    // Clean up
+    let rm_cmd = "rm -f \"" + tmp_file + "\""
+    let rm_pid = process.process_spawn_shell(rm_cmd)
+    if rm_pid > 0
+        process.process_wait(rm_pid)
+    end if
+
+    // If content is empty, staging dir has no real files
+    if str.length(content) == 0
+        return false
+    end if
+
+    // Trim newlines and check again
+    let trimmed = str.trim(content, '\n')
+    if str.length(trimmed) == 0
+        return false
+    end if
+
+    return true
+end validate_staging_dir
+
+fn generate_pkginfo(ctx: BuildContext): bool
+    let pkginfo_path = path.join_path(ctx.pkg_dir, ".PKGINFO")
+
+    mut content = "[package]\n"
+    content = content + "name = \"" + ctx.port.info.name + "\"\n"
+    content = content + "version = \"" + ctx.port.info.version + "\"\n"
+    content = content + "release = " + int_to_str(ctx.port.info.release) + "\n"
+    content = content + "description = \"" + ctx.port.info.description + "\"\n"
+    content = content + "url = \"" + ctx.port.info.url + "\"\n"
+    content = content + "license = \"" + ctx.port.info.license + "\"\n"
+    content = content + "maintainer = \"" + ctx.port.info.maintainer + "\"\n"
+    content = content + "arch = \"" + ctx.port.info.arch + "\"\n"
+
+    // Emit [dependencies] section if there are any
+    let has_runtime = ctx.port.deps.runtime_count > 0
+    let has_build = ctx.port.deps.build_count > 0
+
+    if has_runtime or has_build
+        content = content + "\n[dependencies]\n"
+
+        if has_runtime
+            content = content + "runtime = ["
+            mut i = 0
+            while i < ctx.port.deps.runtime_count
+                if i > 0
+                    content = content + ", "
+                end if
+                content = content + "\"" + ctx.port.deps.runtime[i] + "\""
+                i = i + 1
+            end while
+            content = content + "]\n"
+        end if
+
+        if has_build
+            content = content + "build = ["
+            mut i = 0
+            while i < ctx.port.deps.build_count
+                if i > 0
+                    content = content + ", "
+                end if
+                content = content + "\"" + ctx.port.deps.build[i] + "\""
+                i = i + 1
+            end while
+            content = content + "]\n"
+        end if
+    end if
+
+    return file.writeFile(pkginfo_path, content)
+end generate_pkginfo
+
+fn generate_manifest_file(ctx: BuildContext): bool
+    let manifest_path = path.join_path(ctx.pkg_dir, ".MANIFEST")
+
+    // Generate mtree manifest using native fs operations
+    // BUG-033 (GC candidate overflow) fixed in Reef 0.5.6
+    // uid/gid resolution now uses zero-allocation stat.uid_name()/gid_name()
+    let content = mtree.generate_manifest(ctx.pkg_dir)
+    if str.length(content) == 0
+        return false
+    end if
+
+    return file.writeFile(manifest_path, content)
+end generate_manifest_file
+
+proc cleanup_build(ctx: BuildContext)
+    // Check if cleanup is disabled by --noclean flag
+    if ctx.noclean
+        color.print_info("Skipping cleanup (--noclean specified)")
+        return
+    end if
+
+    // Determine cleanup behavior from config and flags
+    mut clean_work = config.get_clean_work(ctx.cfg)
+    mut clean_pkg = config.get_clean_pkg(ctx.cfg)
+
+    // --clean flag forces cleanup
+    if ctx.force_clean
+        clean_work = true
+        clean_pkg = true
+    end if
+
+    // Clean work directory
+    if clean_work and dir.dir_exists(ctx.work_dir)
+        color.print_info("Cleaning work directory: " + ctx.work_dir)
+        let cmd = "rm -rf \"" + ctx.work_dir + "\""
+        let pid = process.process_spawn_shell(cmd)
+        if pid > 0
+            let exit_code = process.process_wait(pid)
+            if exit_code == 0
+                color.print_success("Cleaned work directory")
+            else
+                color.print_warning("Failed to clean work directory")
+            end if
+        end if
+    end if
+
+    // Clean pkg directory (DESTDIR staging)
+    if clean_pkg and dir.dir_exists(ctx.pkg_dir)
+        color.print_info("Cleaning pkg directory: " + ctx.pkg_dir)
+        let cmd = "rm -rf \"" + ctx.pkg_dir + "\""
+        let pid = process.process_spawn_shell(cmd)
+        if pid > 0
+            let exit_code = process.process_wait(pid)
+            if exit_code == 0
+                color.print_success("Cleaned pkg directory")
+            else
+                color.print_warning("Failed to clean pkg directory")
+            end if
+        end if
+    end if
+end cleanup_build
+
+// Helper: extract filename from URL
+fn extract_filename(url: string): string
+    let last_slash = str.last_index_of_char(url, '/')
+    if last_slash < 0
+        return url
+    end if
+
+    let filename = str.substring(url, last_slash + 1, str.length(url) - last_slash - 1)
+
+    // Remove query string if present
+    let query_pos = str.index_of_char(filename, '?')
+    if query_pos > 0
+        return str.substring(filename, 0, query_pos)
+    end if
+
+    return filename
+end extract_filename
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/clean.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/clean.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,290 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: clean.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Clean command - remove build artifacts and caches
+
+******************************************************************************/
+
+module commands.clean
+
+import sys.args
+import sys.process
+import io.file
+import io.dir
+import io.path
+import core.str
+import core.config
+import types
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(): int
+end export
+
+fn execute(): int
+    let argc = args.count()
+
+    if argc < 3
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    let subcommand = args.get(2)
+
+    // Load config
+    let cfg = config.load()
+
+    if subcommand == "status"
+        return show_status(cfg)
+    elif subcommand == "sources"
+        return clean_sources(cfg)
+    elif subcommand == "packages"
+        return clean_packages(cfg)
+    elif subcommand == "build"
+        return clean_build(cfg)
+    elif subcommand == "all"
+        return clean_all(cfg)
+    else
+        color.print_error("Unknown subcommand: " + subcommand)
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+end execute
+
+fn show_status(cfg: config.Config): int
+    color.print_action("Build artifact status:")
+    println("")
+
+    // Built packages
+    let packages_dir = config.get_packages_dir(cfg)
+    let pkg_count = count_files(packages_dir, ".pkg.tar.xz")
+    mut pkg_protected = ""
+    if config.get_keep_packages(cfg)
+        pkg_protected = " [protected]"
+    end if
+    println("  Packages (" + packages_dir + "):" + pkg_protected)
+    println("    " + int_to_str(pkg_count) + " built package(s)")
+
+    // Source tarballs
+    let sources_dir = config.get_sources_dir(cfg)
+    let src_count = count_files(sources_dir, "")
+    mut src_protected = ""
+    if config.get_keep_sources(cfg)
+        src_protected = " [protected]"
+    end if
+    println("  Sources (" + sources_dir + "):" + src_protected)
+    println("    " + int_to_str(src_count) + " source archive(s)")
+
+    // Build workspace
+    let build_dir = config.get_build_dir(cfg)
+    let build_count = count_subdirs(build_dir)
+    println("  Build (" + build_dir + "):")
+    println("    " + int_to_str(build_count) + " build workspace(s)")
+
+    return ec.EXIT_SUCCESS()
+end show_status
+
+fn clean_sources(cfg: config.Config): int
+    // Respect keep_sources config unless --force is passed
+    if config.get_keep_sources(cfg) and not args.has_flag("force")
+        color.print_warning("Source cache is configured to be preserved (keep_sources = true)")
+        color.print_info("Use --force to override")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    let sources_dir = config.get_sources_dir(cfg)
+    color.print_action("Cleaning source archives...")
+    println("  Location: " + sources_dir)
+
+    if not dir.dir_exists(sources_dir)
+        color.print_info("No sources directory to clean")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    let cmd = "rm -rf \"" + sources_dir + "\"/*"
+    let pid = process.process_spawn_shell(cmd)
+    if pid > 0
+        process.process_wait(pid)
+    end if
+
+    color.print_success("Source archives cleaned")
+    return ec.EXIT_SUCCESS()
+end clean_sources
+
+fn clean_packages(cfg: config.Config): int
+    // Respect keep_packages config unless --force is passed
+    if config.get_keep_packages(cfg) and not args.has_flag("force")
+        color.print_warning("Package cache is configured to be preserved (keep_packages = true)")
+        color.print_info("Use --force to override")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    let packages_dir = config.get_packages_dir(cfg)
+    color.print_action("Cleaning built packages...")
+    println("  Location: " + packages_dir)
+
+    if not dir.dir_exists(packages_dir)
+        color.print_info("No packages directory to clean")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    let cmd = "rm -rf \"" + packages_dir + "\"/*"
+    let pid = process.process_spawn_shell(cmd)
+    if pid > 0
+        process.process_wait(pid)
+    end if
+
+    color.print_success("Built packages cleaned")
+    return ec.EXIT_SUCCESS()
+end clean_packages
+
+fn clean_build(cfg: config.Config): int
+    let build_dir = config.get_build_dir(cfg)
+    color.print_action("Cleaning build workspaces...")
+    println("  Location: " + build_dir)
+
+    if not dir.dir_exists(build_dir)
+        color.print_info("No build directory to clean")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    let cmd = "rm -rf \"" + build_dir + "\"/*"
+    let pid = process.process_spawn_shell(cmd)
+    if pid > 0
+        process.process_wait(pid)
+    end if
+
+    color.print_success("Build workspaces cleaned")
+    return ec.EXIT_SUCCESS()
+end clean_build
+
+fn clean_all(cfg: config.Config): int
+    color.print_action("Cleaning all build artifacts...")
+    println("")
+
+    clean_sources(cfg)
+    clean_packages(cfg)
+    clean_build(cfg)
+
+    println("")
+    color.print_success("All build artifacts cleaned")
+    return ec.EXIT_SUCCESS()
+end clean_all
+
+fn count_files(dir_path: string, extension: string): int
+    if not dir.dir_exists(dir_path)
+        return 0
+    end if
+
+    mut entries: [string] = new [string](512)
+    let count = dir.list_dir(dir_path, entries, 512)
+
+    if str.length(extension) == 0
+        return count
+    end if
+
+    mut matched = 0
+    mut i = 0
+    while i < count
+        if str.ends_with(entries[i], extension)
+            matched = matched + 1
+        end if
+        i = i + 1
+    end while
+
+    return matched
+end count_files
+
+fn count_subdirs(dir_path: string): int
+    if not dir.dir_exists(dir_path)
+        return 0
+    end if
+
+    mut entries: [string] = new [string](256)
+    let count = dir.list_dir(dir_path, entries, 256)
+
+    // Count only directories (packages being built)
+    mut dirs = 0
+    mut i = 0
+    while i < count
+        let entry = entries[i]
+        if str.length(entry) > 0 and entry[0] != '.'
+            let full_path = path.join_path(dir_path, entry)
+            if dir.dir_exists(full_path)
+                dirs = dirs + 1
+            end if
+        end if
+        i = i + 1
+    end while
+
+    return dirs
+end count_subdirs
+
+proc print_usage()
+    println("Usage: coral clean <target>")
+    println("")
+    println("Remove build artifacts and cached files.")
+    println("")
+    println("Targets:")
+    println("  status    Show what can be cleaned")
+    println("  sources   Remove downloaded source archives")
+    println("  packages  Remove built binary packages")
+    println("  build     Remove build workspaces")
+    println("  all       Remove all of the above")
+    println("")
+    println("Locations:")
+    println("  sources:  /usr/zports/pkgs/sources/")
+    println("  packages: /usr/zports/pkgs/packages/")
+    println("  build:    /usr/zports/pkgs/build/")
+    println("")
+    println("Options:")
+    println("  --force   Override keep_packages/keep_sources config protection")
+    println("")
+    println("Note: This does NOT affect installed packages or the package database.")
+    println("      Use 'coral remove' to uninstall packages.")
+    println("")
+    println("Examples:")
+    println("  coral clean status")
+    println("  coral clean build")
+    println("  coral clean all")
+end print_usage
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/db.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/db.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,308 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: db.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Database command - manage package database indexes
+
+******************************************************************************/
+
+module commands.db
+
+import sys.args
+import io.file
+import io.dir
+import core.str
+import core.config
+import core.pkgdb
+import core.portindex
+import types
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(opts: types.GlobalOptions): int
+end export
+
+fn execute(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    // Subcommand should be at cmd_index + 1
+    if argc <= opts.cmd_index + 1
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    // Load configuration and apply command-line overrides
+    let base_cfg = config.load()
+    let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix)
+    let root_path = config.get_root(cfg)
+
+    // Show root if set (verbose mode)
+    if opts.verbose and str.length(root_path) > 0
+        color.print_info("Using root: " + root_path)
+    end if
+
+    let subcommand = args.get(opts.cmd_index + 1)
+
+    if subcommand == "init"
+        return db_init(root_path)
+    elif subcommand == "rebuild"
+        return db_rebuild(root_path)
+    elif subcommand == "status"
+        return db_status(root_path)
+    elif subcommand == "dump"
+        return db_dump(root_path)
+    else
+        color.print_error("Unknown subcommand: " + subcommand)
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+end execute
+
+fn db_init(root_path: string): int
+    if str.length(root_path) > 0
+        color.print_action("Initializing package database in " + root_path + "...")
+    else
+        color.print_action("Initializing package database...")
+    end if
+    println("")
+
+    // Check if database already exists (use rooted version if root is set)
+    let exists = check_indexes_exist(root_path)
+    if exists
+        color.print_warning("Database indexes already exist")
+        color.print_info("Use 'coral db rebuild' to regenerate indexes")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Initialize the database directory (use rooted version if root is set)
+    if not init_pkgdb(root_path)
+        color.print_error("Failed to create database directory")
+        return ec.EXIT_DB_ERROR()
+    end if
+
+    // Build initial indexes (use rooted version if root is set)
+    if not rebuild_pkgdb(root_path)
+        color.print_error("Failed to build initial indexes")
+        return ec.EXIT_DB_ERROR()
+    end if
+
+    // Build ports index
+    portindex.rebuild_port_index()
+
+    let stats = get_pkgdb_stats(root_path)
+    let port_count = portindex.get_port_count()
+    color.print_success("Database initialized successfully")
+    println("  Packages indexed: " + int_to_str(stats.package_count))
+    println("  Files indexed: " + int_to_str(stats.file_count))
+    println("  Ports indexed: " + int_to_str(port_count))
+
+    return ec.EXIT_SUCCESS()
+end db_init
+
+fn db_rebuild(root_path: string): int
+    if str.length(root_path) > 0
+        color.print_action("Rebuilding package database indexes in " + root_path + "...")
+    else
+        color.print_action("Rebuilding package database indexes...")
+    end if
+    println("")
+
+    // Check if db directory exists (use rooted version if root is set)
+    if not init_pkgdb(root_path)
+        color.print_error("Failed to access database directory")
+        color.print_info("Run 'coral db init' first")
+        return ec.EXIT_DB_ERROR()
+    end if
+
+    // Rebuild indexes from source of truth (use rooted version if root is set)
+    if not rebuild_pkgdb(root_path)
+        color.print_error("Failed to rebuild indexes")
+        return ec.EXIT_DB_ERROR()
+    end if
+
+    // Rebuild ports index
+    portindex.rebuild_port_index()
+
+    let stats = get_pkgdb_stats(root_path)
+    let port_count = portindex.get_port_count()
+    color.print_success("Database indexes rebuilt successfully")
+    println("  Packages indexed: " + int_to_str(stats.package_count))
+    println("  Files indexed: " + int_to_str(stats.file_count))
+    println("  Ports indexed: " + int_to_str(port_count))
+
+    return ec.EXIT_SUCCESS()
+end db_rebuild
+
+fn db_dump(root_path: string): int
+    color.print_action("Dumping files index contents...")
+    println("")
+
+    mut paths: [string] = new [string](256)
+    mut owners: [string] = new [string](256)
+    let count = pkgdb.dump_files_index_rooted(root_path, paths, owners, 256)
+
+    if count == 0
+        color.print_warning("No files in index or index not loaded")
+        return ec.EXIT_DB_ERROR()
+    end if
+
+    println("Files in index: " + int_to_str(count))
+    println("")
+
+    mut i = 0
+    while i < count
+        println("  [" + int_to_str(i) + "] '" + paths[i] + "' -> '" + owners[i] + "'")
+        i = i + 1
+    end while
+
+    return ec.EXIT_SUCCESS()
+end db_dump
+
+fn db_status(root_path: string): int
+    if str.length(root_path) > 0
+        color.print_action("Package database status for " + root_path + ":")
+    else
+        color.print_action("Package database status:")
+    end if
+    println("")
+
+    let stats = get_pkgdb_stats(root_path)
+
+    if not check_indexes_exist(root_path)
+        color.print_warning("Database indexes not found")
+        color.print_info("Run 'coral db init' to create indexes")
+        println("")
+        if str.length(root_path) > 0
+            println("  Source of truth: " + root_path + "/var/lib/coral/installed/")
+        else
+            println("  Source of truth: /var/lib/coral/installed/")
+        end if
+        println("  Packages installed: " + int_to_str(stats.package_count))
+        return ec.EXIT_DB_INDEX_STALE()
+    end if
+
+    if str.length(root_path) > 0
+        println("  Index location: " + root_path + types.get_index_dir())
+    else
+        println("  Index location: " + types.get_index_dir())
+    end if
+    println("  Packages indexed: " + int_to_str(stats.package_count))
+    println("  Files indexed: " + int_to_str(stats.file_count))
+
+    if stats.packages_db_size > 0
+        println("  packages.db size: " + format_size(stats.packages_db_size))
+    end if
+    if stats.files_db_size > 0
+        println("  files.db size: " + format_size(stats.files_db_size))
+    end if
+
+    // Ports index status
+    if portindex.port_index_exists()
+        let port_count = portindex.get_port_count()
+        println("  Ports indexed: " + int_to_str(port_count))
+    else
+        println("  Ports index: not built")
+    end if
+
+    return ec.EXIT_SUCCESS()
+end db_status
+
+// Helper functions for rooted pkgdb operations
+fn check_indexes_exist(root_path: string): bool
+    if str.length(root_path) > 0
+        return pkgdb.indexes_exist_rooted(root_path)
+    end if
+    return pkgdb.indexes_exist()
+end check_indexes_exist
+
+fn init_pkgdb(root_path: string): bool
+    if str.length(root_path) > 0
+        return pkgdb.init_db_rooted(root_path)
+    end if
+    return pkgdb.init_db()
+end init_pkgdb
+
+fn rebuild_pkgdb(root_path: string): bool
+    if str.length(root_path) > 0
+        return pkgdb.rebuild_indexes_rooted(root_path)
+    end if
+    return pkgdb.rebuild_indexes()
+end rebuild_pkgdb
+
+fn get_pkgdb_stats(root_path: string): pkgdb.IndexStats
+    if str.length(root_path) > 0
+        return pkgdb.get_index_stats_rooted(root_path)
+    end if
+    return pkgdb.get_index_stats()
+end get_pkgdb_stats
+
+fn format_size(bytes: int): string
+    if bytes < 1024
+        return int_to_str(bytes) + " B"
+    elif bytes < 1024 * 1024
+        return int_to_str(bytes / 1024) + " KB"
+    elif bytes < 1024 * 1024 * 1024
+        return int_to_str(bytes / (1024 * 1024)) + " MB"
+    else
+        return int_to_str(bytes / (1024 * 1024 * 1024)) + " GB"
+    end if
+end format_size
+
+proc print_usage()
+    println("Usage: coral db <command>")
+    println("")
+    println("Manage package database indexes.")
+    println("")
+    println("Commands:")
+    println("  init      Initialize database (run once during system setup)")
+    println("  rebuild   Rebuild indexes from installed packages")
+    println("  status    Show database status and statistics")
+    println("")
+    println("The package database provides fast lookups for installed packages")
+    println("and file ownership. The source of truth is always the installed/")
+    println("directory; indexes can be safely rebuilt at any time.")
+    println("")
+    println("Examples:")
+    println("  coral db init")
+    println("  coral db rebuild")
+    println("  coral db status")
+end print_usage
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/depends.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/depends.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,276 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: depends.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Depends command - show package dependency tree
+
+******************************************************************************/
+
+module commands.depends
+
+import sys.args
+import core.str
+import core.port
+import core.portindex
+import core.database
+import core.resolver
+import types
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(): int
+end export
+
+fn execute(): int
+    let argc = args.count()
+
+    if argc < 3
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    let pkg_name = args.get(2)
+
+    // Check for --reverse flag (show what depends on this package)
+    if args.has_flag("reverse") or args.has_flag("r")
+        show_reverse_deps(pkg_name)
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Show forward dependencies
+    show_deps(pkg_name)
+    return ec.EXIT_SUCCESS()
+end execute
+
+proc show_deps(name: string)
+    // Find the port
+    let result = port.find(name)
+    if not result.success
+        color.print_error("Port not found: " + name)
+        return
+    end if
+
+    let p = result.port
+
+    color.print_action("Dependencies for " + name + ":")
+    println("")
+
+    // Show runtime dependencies
+    if p.deps.runtime_count == 0
+        color.print_info("No runtime dependencies")
+    else
+        color.print_info("Runtime dependencies:")
+        show_dep_tree(name, 0)
+    end if
+
+    println("")
+
+    // Show build dependencies
+    if p.deps.build_count > 0
+        color.print_info("Build dependencies:")
+        mut i = 0
+        while i < p.deps.build_count
+            print("  ")
+            let dep = p.deps.build[i]
+            print(dep)
+            if database.is_installed(dep)
+                println(" [installed]")
+            else
+                println("")
+            end if
+            i = i + 1
+        end while
+    end if
+end show_deps
+
+proc show_dep_tree(name: string, depth: int)
+    if depth > 10
+        return
+    end if
+
+    let result = port.find(name)
+    if not result.success
+        return
+    end if
+
+    let p = result.port
+
+    mut i = 0
+    while i < p.deps.runtime_count
+        let dep = p.deps.runtime[i]
+
+        // Print with indentation
+        print_indent(depth)
+        print(dep)
+        if database.is_installed(dep)
+            println(" [installed]")
+        else
+            println("")
+        end if
+
+        // Recurse
+        show_dep_tree(dep, depth + 1)
+
+        i = i + 1
+    end while
+end show_dep_tree
+
+proc show_reverse_deps(name: string)
+    color.print_action("Packages that depend on " + name + ":")
+    println("")
+
+    mut found = 0
+
+    // Try ports index first (fast path)
+    if portindex.port_index_exists()
+        mut results: [portindex.PortIndexEntry] = new [portindex.PortIndexEntry](512)
+        let result_count = portindex.find_reverse_deps(name, results, 512)
+
+        mut i = 0
+        while i < result_count
+            let entry = results[i]
+            print("  ")
+            print(entry.category)
+            print("/")
+            print(entry.name)
+            if database.is_installed(entry.name)
+                println(" [installed]")
+            else
+                println("")
+            end if
+            found = found + 1
+            i = i + 1
+        end while
+    else
+        // Fall back to filesystem scan
+        mut cats: [string] = new [string](32)
+        let cat_count = port.get_categories(cats, 32)
+
+        mut i = 0
+        while i < cat_count
+            let category = cats[i]
+            mut ports: [string] = new [string](256)
+            let port_count = port.list_category(category, ports, 256)
+
+            mut j = 0
+            while j < port_count
+                let port_name = ports[j]
+                if depends_on(port_name, name)
+                    print("  ")
+                    print(category)
+                    print("/")
+                    print(port_name)
+                    if database.is_installed(port_name)
+                        println(" [installed]")
+                    else
+                        println("")
+                    end if
+                    found = found + 1
+                end if
+                j = j + 1
+            end while
+
+            i = i + 1
+        end while
+    end if
+
+    if found == 0
+        color.print_info("No packages depend on " + name)
+    else
+        println("")
+        color.print_info("Total: " + int_to_str(found) + " package(s)")
+    end if
+end show_reverse_deps
+
+fn depends_on(pkg_name: string, dep_name: string): bool
+    let result = port.find(pkg_name)
+    if not result.success
+        return false
+    end if
+
+    let p = result.port
+
+    // Check runtime deps
+    mut i = 0
+    while i < p.deps.runtime_count
+        if p.deps.runtime[i] == dep_name
+            return true
+        end if
+        i = i + 1
+    end while
+
+    // Check build deps
+    i = 0
+    while i < p.deps.build_count
+        if p.deps.build[i] == dep_name
+            return true
+        end if
+        i = i + 1
+    end while
+
+    return false
+end depends_on
+
+proc print_indent(depth: int)
+    mut i = 0
+    while i < depth
+        print("  ")
+        i = i + 1
+    end while
+    print("  ")  // Base indent
+end print_indent
+
+proc print_usage()
+    println("Usage: coral depends <package> [options]")
+    println("")
+    println("Show dependency tree for a package.")
+    println("")
+    println("Arguments:")
+    println("  <package>    Name of package to show dependencies for")
+    println("")
+    println("Options:")
+    println("  -r, --reverse    Show packages that depend on this package")
+    println("")
+    println("Examples:")
+    println("  coral depends vim")
+    println("  coral depends vim --reverse")
+end print_usage
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/files.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/files.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,137 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: files.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Files command - list files owned by a package
+
+******************************************************************************/
+
+module commands.files
+
+import sys.args
+import core.str
+import core.config
+import core.database
+import types
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(opts: types.GlobalOptions): int
+end export
+
+fn execute(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    // Package name should be at cmd_index + 1
+    if argc <= opts.cmd_index + 1
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    // Load configuration and apply command-line overrides
+    let base_cfg = config.load()
+    let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix)
+    let root_path = config.get_root(cfg)
+
+    // Show root if set (verbose mode)
+    if opts.verbose and str.length(root_path) > 0
+        color.print_info("Using root: " + root_path)
+    end if
+
+    let pkg_name = args.get(opts.cmd_index + 1)
+
+    // Check if installed (use rooted version if root is set)
+    if not check_installed(pkg_name, root_path)
+        color.print_error(pkg_name + " is not installed")
+        return ec.EXIT_PKG_NOT_FOUND()
+    end if
+
+    // Get files (use rooted version if root is set)
+    mut files: [string] = new [string](4096)
+    let file_count = get_files_list(pkg_name, files, 4096, root_path)
+
+    if file_count == 0
+        color.print_info("No files recorded for " + pkg_name)
+        return ec.EXIT_SUCCESS()
+    end if
+
+    color.print_action("Files owned by " + pkg_name + ":")
+    println("")
+
+    mut i = 0
+    while i < file_count
+        println("/" + files[i])
+        i = i + 1
+    end while
+
+    println("")
+    color.print_info("Total: " + int_to_str(file_count) + " files")
+    return ec.EXIT_SUCCESS()
+end execute
+
+// Helper functions for rooted operations
+fn check_installed(name: string, root_path: string): bool
+    if str.length(root_path) > 0
+        return database.is_installed_rooted(name, root_path)
+    end if
+    return database.is_installed(name)
+end check_installed
+
+fn get_files_list(name: string, files: [string], max_count: int, root_path: string): int
+    if str.length(root_path) > 0
+        return database.get_files_rooted(name, files, max_count, root_path)
+    end if
+    return database.get_files(name, files, max_count)
+end get_files_list
+
+proc print_usage()
+    println("Usage: coral files <package>")
+    println("")
+    println("List all files owned by an installed package.")
+    println("")
+    println("Arguments:")
+    println("  <package>    Name of installed package")
+    println("")
+    println("Examples:")
+    println("  coral files vim")
+    println("  coral files hello")
+end print_usage
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/info.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/info.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,198 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: info.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Info command - display package information
+
+******************************************************************************/
+
+module commands.info
+
+import sys.args
+import core.str
+import core.config
+import core.database
+import core.port
+import types
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(opts: types.GlobalOptions): int
+end export
+
+fn execute(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    // Package name should be at cmd_index + 1
+    if argc <= opts.cmd_index + 1
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    // Load configuration and apply command-line overrides
+    let base_cfg = config.load()
+    let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix)
+    let root_path = config.get_root(cfg)
+
+    // Show root if set (verbose mode)
+    if opts.verbose and str.length(root_path) > 0
+        color.print_info("Using root: " + root_path)
+    end if
+
+    let pkg_name = args.get(opts.cmd_index + 1)
+
+    // Check if installed (use rooted version if root is set)
+    let is_installed = check_installed(pkg_name, root_path)
+
+    // Find port
+    let port_result = port.find(pkg_name)
+
+    if not is_installed and not port_result.success
+        color.print_error("Package not found: " + pkg_name)
+        return ec.EXIT_PKG_NOT_FOUND()
+    end if
+
+    // Print package info
+    if is_installed
+        let installed_pkg = get_installed_pkg(pkg_name, root_path)
+        print_installed_info(installed_pkg)
+
+        if port_result.success
+            let p = port_result.port
+            // Check if upgrade available
+            if str.compare(installed_pkg.info.version, p.info.version) < 0
+                println("")
+                color.print_info("Upgrade available: " + p.info.version)
+            end if
+        end if
+    else
+        // Not installed, show port info
+        let p = port_result.port
+        print_port_info(p)
+    end if
+    return ec.EXIT_SUCCESS()
+end execute
+
+// Helper functions for rooted operations
+fn check_installed(name: string, root_path: string): bool
+    if str.length(root_path) > 0
+        return database.is_installed_rooted(name, root_path)
+    end if
+    return database.is_installed(name)
+end check_installed
+
+fn get_installed_pkg(name: string, root_path: string): types.InstalledPackage
+    if str.length(root_path) > 0
+        return database.get_installed_rooted(name, root_path)
+    end if
+    return database.get_installed(name)
+end get_installed_pkg
+
+proc print_usage()
+    println("Usage: coral info <package>")
+    println("")
+    println("Display detailed information about a package.")
+    println("")
+    println("Arguments:")
+    println("  <package>    Name of package to show info for")
+    println("")
+    println("Examples:")
+    println("  coral info vim")
+    println("  coral info hello")
+end print_usage
+
+proc print_installed_info(pkg: types.InstalledPackage)
+    color.print_action("Package: " + pkg.info.name)
+    println("")
+    println("Status:       installed")
+    println("Version:      " + pkg.info.version + "-" + int_to_str(pkg.info.release))
+    println("Description:  " + pkg.info.description)
+    println("URL:          " + pkg.info.url)
+    println("License:      " + pkg.info.license)
+    println("Maintainer:   " + pkg.info.maintainer)
+    println("Architecture: " + pkg.info.arch)
+    println("")
+    println("Install date: " + pkg.install_date)
+    println("Reason:       " + pkg.install_reason)
+    println("Files:        " + int_to_str(pkg.files_count))
+end print_installed_info
+
+proc print_port_info(p: types.Port)
+    color.print_action("Port: " + p.info.name)
+    println("")
+    println("Status:       not installed")
+    println("Version:      " + p.info.version + "-" + int_to_str(p.info.release))
+    println("Description:  " + p.info.description)
+    println("URL:          " + p.info.url)
+    println("License:      " + p.info.license)
+    println("Maintainer:   " + p.info.maintainer)
+    println("Architecture: " + p.info.arch)
+
+    // Show dependencies if any
+    if p.deps.runtime_count > 0
+        println("")
+        print("Depends on:   ")
+        mut i = 0
+        while i < p.deps.runtime_count
+            if i > 0
+                print(", ")
+            end if
+            print(p.deps.runtime[i])
+            i = i + 1
+        end while
+        println("")
+    end if
+
+    if p.deps.build_count > 0
+        println("")
+        print("Build deps:   ")
+        mut i = 0
+        while i < p.deps.build_count
+            if i > 0
+                print(", ")
+            end if
+            print(p.deps.build[i])
+            i = i + 1
+        end while
+        println("")
+    end if
+end print_port_info
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/install.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/install.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,836 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: install.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Install command - install packages from archives
+
+******************************************************************************/
+
+module commands.install
+
+import sys.args
+import sys.process
+import io.file
+import io.dir
+import io.path
+import core.str
+import core.config
+import core.database
+import core.pkgdb
+import core.package
+import core.groups
+import core.repository
+import core.signing
+import core.resolver
+import core.port
+import core.portindex
+import util.checksum
+import util.mtree
+import util.http as uhttp
+import util.prompt
+import util.version as ver
+import types
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(opts: types.GlobalOptions): int
+end export
+
+fn execute(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    // Package arguments start after the command
+    if argc <= opts.cmd_index + 1
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    // Load configuration and apply command-line overrides
+    let base_cfg = config.load()
+    let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix)
+    uhttp.enable_proxy(config.get_use_proxy(cfg))
+
+    // Auto-refresh port index if configured
+    if config.get_auto_refresh(cfg)
+        portindex.rebuild_port_index()
+    end if
+
+    // Show root/prefix if set (verbose mode)
+    if opts.verbose
+        let root = config.get_root(cfg)
+        let prefix = config.get_prefix(cfg)
+        if str.length(root) > 0
+            color.print_info("Using root: " + root)
+        end if
+        if str.length(prefix) > 0
+            color.print_info("Using prefix: " + prefix)
+        end if
+    end if
+
+    // Collect all packages to install (expanding groups)
+    mut packages: [string] = new [string](256)
+    mut pkg_count = 0
+    mut direct_files: [string] = new [string](16)
+    mut direct_count = 0
+
+    // Process each package argument (starting after the command)
+    mut i = opts.cmd_index + 1
+    while i < argc and pkg_count < 256
+        let pkg_arg = args.get(i)
+
+        // Skip flags and their values
+        if pkg_arg == "--root" or pkg_arg == "--prefix"
+            i = i + 2  // Skip flag and its value
+            continue
+        end if
+        if str.starts_with(pkg_arg, "-")
+            i = i + 1
+            continue
+        end if
+
+        // Check if this is a direct file path to a .pkg.tar.xz
+        if str.ends_with(pkg_arg, ".pkg.tar.xz") and file.fileExists(pkg_arg)
+            direct_files[direct_count] = pkg_arg
+            direct_count = direct_count + 1
+            i = i + 1
+            continue
+        end if
+
+        // Check if this is a group reference
+        if groups.is_group(pkg_arg)
+            // Expand group to member packages
+            mut group_members: [string] = new [string](128)
+            let member_count = groups.expand_group(pkg_arg, group_members, 128)
+
+            if member_count == 0
+                color.print_error("Group not found or empty: " + pkg_arg)
+                i = i + 1
+                continue
+            end if
+
+            color.print_info("Expanding group " + pkg_arg + " (" + int_to_str(member_count) + " packages)")
+
+            // Add all group members to packages list
+            mut j = 0
+            while j < member_count and pkg_count < 256
+                packages[pkg_count] = group_members[j]
+                pkg_count = pkg_count + 1
+                j = j + 1
+            end while
+        else
+            // Regular package
+            packages[pkg_count] = pkg_arg
+            pkg_count = pkg_count + 1
+        end if
+
+        i = i + 1
+    end while
+
+    // Handle direct file installs — bypass resolver, install immediately
+    if direct_count > 0
+        let root = get_root(cfg)
+        let root_for_db = config.get_root(cfg)
+        if str.length(root_for_db) > 0
+            if not database.init_db_rooted(root_for_db)
+                color.print_error("Failed to initialize database in " + root_for_db)
+                return ec.EXIT_DB_ERROR()
+            end if
+        else
+            if not database.init_db()
+                color.print_error("Failed to initialize package database")
+                return ec.EXIT_DB_ERROR()
+            end if
+        end if
+
+        mut di = 0
+        while di < direct_count
+            if not install_package(direct_files[di], cfg, root_for_db, opts)
+                color.print_error("Failed to install: " + direct_files[di])
+                return ec.EXIT_INSTALL_FAILED()
+            end if
+            di = di + 1
+        end while
+
+        if pkg_count == 0
+            return ec.EXIT_SUCCESS()
+        end if
+    end if
+
+    // Resolve dependencies for all packages
+    // Use rooted resolver if installing to alternate root
+    mut install_order: [string] = new [string](512)
+    let root_for_resolve = config.get_root(cfg)
+    mut install_count = 0
+    if str.length(root_for_resolve) > 0
+        install_count = resolver.get_install_order_rooted(packages, pkg_count, install_order, 512, root_for_resolve)
+    else
+        install_count = resolver.get_install_order(packages, pkg_count, install_order, 512)
+    end if
+
+    // When force/reinstall is set, add requested packages that the resolver
+    // skipped (because they're already installed) back into the install order
+    if opts.force and install_count < pkg_count
+        mut fi = 0
+        while fi < pkg_count
+            let pkg = packages[fi]
+            if not array_contains(install_order, install_count, pkg)
+                install_order[install_count] = pkg
+                install_count = install_count + 1
+            end if
+            fi = fi + 1
+        end while
+    end if
+
+    if install_count == 0 and pkg_count > 0
+        color.print_warning("No packages to install (all already installed or not found)")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Separate new installs from already-installed (for display)
+    mut to_install: [string] = new [string](512)
+    mut install_idx = 0
+
+    mut dep_idx = 0
+    while dep_idx < install_count
+        to_install[install_idx] = install_order[dep_idx]
+        install_idx = install_idx + 1
+        dep_idx = dep_idx + 1
+    end while
+
+    // Ask for confirmation (unless -y flag)
+    mut empty_upgrade: [string] = new [string](1)
+    if not prompt.confirm_install(to_install, install_idx, empty_upgrade, 0, opts)
+        color.print_info("Installation cancelled")
+        return ec.EXIT_CANCELLED()
+    end if
+
+    // Dry-run: show what would be done and exit
+    if opts.dry_run
+        color.print_info("Dry run — no changes made")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Get root path for installation
+    let root = get_root(cfg)
+
+    // Initialize database in the target root
+    let root_for_db = config.get_root(cfg)
+    if str.length(root_for_db) > 0
+        if not database.init_db_rooted(root_for_db)
+            color.print_error("Failed to initialize database in " + root_for_db)
+            return ec.EXIT_DB_ERROR()
+        end if
+    else
+        if not database.init_db()
+            color.print_error("Failed to initialize package database")
+            return ec.EXIT_DB_ERROR()
+        end if
+    end if
+
+    // Install all packages in resolved order (dependencies first)
+    i = 0
+    while i < install_count
+        let pkg_arg = install_order[i]
+
+        // Install this package
+        if not install_package(pkg_arg, cfg, root_for_db, opts)
+            color.print_error("Failed to install: " + pkg_arg)
+            color.print_error("Aborting installation - dependency failed")
+            return ec.EXIT_INSTALL_FAILED()
+        end if
+
+        i = i + 1
+    end while
+
+    return ec.EXIT_SUCCESS()
+end execute
+
+// Helper to check if array contains a string
+fn array_contains(arr: [string], count: int, s: string): bool
+    mut i = 0
+    while i < count
+        if str.equals(arr[i], s)
+            return true
+        end if
+        i = i + 1
+    end while
+    return false
+end array_contains
+
+proc print_usage()
+    println("Usage: coral install <package...>")
+    println("")
+    println("Install one or more packages.")
+    println("")
+    println("Arguments:")
+    println("  <package>    Package name or path to .pkg.tar.xz file")
+    println("")
+    println("Options:")
+    println("  -y, --yes    Don't ask for confirmation")
+    println("  --force      Force reinstall if already installed")
+    println("")
+    println("Examples:")
+    println("  coral install vim")
+    println("  coral install /path/to/package.pkg.tar.xz")
+    println("  coral install vim nano htop")
+end print_usage
+
+fn install_package(pkg_arg: string, cfg: config.Config, root_path: string, opts: types.GlobalOptions): bool
+    // Determine if this is a file path or package name
+    let pkg_path = resolve_package_path(pkg_arg, cfg)
+
+    if str.length(pkg_path) == 0
+        color.print_error("Package not found: " + pkg_arg)
+        return false
+    end if
+
+    // Verify signature if required (for local packages not already verified by resolve_package_path)
+    let require_signed = config.get_require_signed_packages(cfg)
+    if require_signed
+        let sig_path = pkg_path + ".sig"
+        if file.fileExists(sig_path)
+            color.print_info("Verifying package signature...")
+            let verify_result = signing.verify_package(pkg_path, sig_path)
+            if not verify_result.success or not verify_result.valid
+                color.print_error("Signature verification failed: " + verify_result.error)
+                return false
+            end if
+            color.print_success("Signature verified (key: " + verify_result.keyid + ")")
+        else
+            color.print_error("Package signature required but not found: " + sig_path)
+            return false
+        end if
+    end if
+
+    color.print_action("Installing " + pkg_arg + "...")
+
+    // Create extraction directory
+    let extract_dir = create_extract_dir(cfg)
+    if str.length(extract_dir) == 0
+        color.print_error("Failed to create extraction directory")
+        return false
+    end if
+
+    // Extract package
+    color.print_info("Extracting package...")
+    let result = package.extract_package(pkg_path, extract_dir)
+    if not result.success
+        color.print_error(result.error)
+        cleanup_extract_dir(extract_dir)
+        return false
+    end if
+
+    // Read package info
+    let info = package.read_pkginfo(extract_dir)
+    if str.length(info.name) == 0
+        color.print_error("Failed to read package info")
+        cleanup_extract_dir(extract_dir)
+        return false
+    end if
+
+    color.print_info("Package: " + info.name + " " + info.version)
+
+    // Check for conflicts with installed packages
+    if info.conflicts_count > 0
+        mut ci = 0
+        while ci < info.conflicts_count
+            let conflict = info.conflicts[ci]
+            if check_installed(conflict, root_path)
+                color.print_error("Package " + info.name + " conflicts with installed package: " + conflict)
+                if not opts.force
+                    color.print_error("Use --force to override conflict check")
+                    cleanup_extract_dir(extract_dir)
+                    return false
+                end if
+                color.print_warning("Proceeding despite conflict (--force specified)")
+            end if
+            ci = ci + 1
+        end while
+    end if
+
+    // Check if already installed (in target root)
+    let already_installed = check_installed(info.name, root_path)
+    mut is_upgrade = false
+    mut old_version = ""
+
+    if already_installed
+        // Get old package info to check version
+        let old_pkg = get_old_installed(info.name, root_path)
+        old_version = old_pkg.info.version
+
+        if not str.equals(old_version, info.version)
+            // Different version — upgrade
+            is_upgrade = true
+            color.print_info("Upgrading " + info.name + " " + old_version + " -> " + info.version)
+        else
+            // Same version — skip or reinstall with --force
+            if not opts.force
+                color.print_warning(info.name + " " + info.version + " is already installed (use --force to reinstall)")
+                cleanup_extract_dir(extract_dir)
+                return true
+            end if
+            color.print_info("Reinstalling " + info.name + "...")
+        end if
+    end if
+
+    let install_root = get_root(cfg)
+
+    if is_upgrade
+        // Upgrade flow: pre-upgrade → remove old files → install new files → post-upgrade
+
+        // Run pre-upgrade from stored scripts (old package)
+        let old_scripts = database.get_scripts_dir(info.name)
+        if database.has_stored_scripts(info.name)
+            color.print_info("Running pre-upgrade script...")
+            if not package.run_stored_pre_upgrade(old_scripts, old_version, info.version)
+                color.print_error("Pre-upgrade script failed, aborting")
+                cleanup_extract_dir(extract_dir)
+                return false
+            end if
+        end if
+
+        // Remove old files
+        color.print_info("Removing old version files...")
+        mut old_files: [string] = new [string](4096)
+        let old_file_count = get_old_files(info.name, old_files, 4096, root_path)
+        if old_file_count > 0
+            package.remove_files(old_files, old_file_count, install_root)
+        end if
+
+        // Load old manifest for config file checksum protection
+        mut old_manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192)
+        let old_manifest_count = load_old_manifest(info.name, root_path, old_manifest_entries, 8192)
+
+        // Install new files (with upgrade config protection)
+        color.print_info("Installing files...")
+        if old_manifest_count > 0
+            if not package.install_files_upgrade(extract_dir, install_root, old_manifest_entries, old_manifest_count)
+                color.print_error("Failed to install files")
+                cleanup_extract_dir(extract_dir)
+                return false
+            end if
+        else
+            if not package.install_files(extract_dir, install_root)
+                color.print_error("Failed to install files")
+                cleanup_extract_dir(extract_dir)
+                return false
+            end if
+        end if
+
+        // Run post-upgrade from new package's scripts
+        if package.has_scripts(extract_dir)
+            color.print_info("Running post-upgrade script...")
+            if not package.run_post_upgrade(extract_dir, old_version, info.version)
+                color.print_warning("Post-upgrade script failed")
+            end if
+        end if
+    else
+        // Fresh install flow
+
+        // Run pre-install script (abort on failure)
+        if package.has_scripts(extract_dir)
+            color.print_info("Running pre-install script...")
+            if not package.run_pre_install(extract_dir, info.version)
+                color.print_error("Pre-install script failed")
+                cleanup_extract_dir(extract_dir)
+                return false
+            end if
+        end if
+
+        // Install files to target root
+        color.print_info("Installing files...")
+        if not package.install_files(extract_dir, install_root)
+            color.print_error("Failed to install files")
+            cleanup_extract_dir(extract_dir)
+            return false
+        end if
+
+        // Run post-install script (warn on failure, don't abort)
+        if package.has_scripts(extract_dir)
+            color.print_info("Running post-install script...")
+            if not package.run_post_install(extract_dir, info.version)
+                color.print_warning("Post-install script failed")
+            end if
+        end if
+    end if
+
+    // Register in database (use rooted version if root is set)
+    color.print_info("Registering package...")
+    if not register_installed_fast_rooted(info, extract_dir, root_path)
+        color.print_error("Failed to register package in database")
+        // Files are installed but not registered - warn but don't fail
+    end if
+
+    // Store scripts for later use (removal)
+    if package.has_scripts(extract_dir)
+        if not store_scripts_rooted(info.name, extract_dir, root_path)
+            color.print_warning("Failed to store scripts for removal")
+        end if
+    end if
+
+    // Store config files list for upgrade handling
+    if not store_config_files_rooted(info.name, extract_dir, root_path)
+        color.print_warning("Failed to store config files list")
+    end if
+
+    // Update package database indexes
+    if not pkgdb.rebuild_indexes_rooted(root_path)
+        color.print_warning("Failed to update package indexes")
+    end if
+
+    // Cleanup
+    cleanup_extract_dir(extract_dir)
+
+    color.print_success("Installed " + info.name + " " + info.version)
+    return true
+end install_package
+
+// Check if package is installed, using rooted check if root is set
+fn check_installed(name: string, root_path: string): bool
+    if str.length(root_path) > 0
+        return database.is_installed_rooted(name, root_path)
+    end if
+    return database.is_installed(name)
+end check_installed
+
+// Register package using rooted version if root is set
+fn register_installed_fast_rooted(info: types.PackageInfo, extract_dir: string, root_path: string): bool
+    mut files: [string] = new [string](1)
+    mut pkg = new_installed_pkg(info, files, 0)
+
+    if str.length(root_path) > 0
+        return database.register_from_extract_dir_rooted(pkg, extract_dir, root_path)
+    end if
+    return database.register_from_extract_dir(pkg, extract_dir)
+end register_installed_fast_rooted
+
+// Store scripts using rooted version if root is set
+fn store_scripts_rooted(name: string, extract_dir: string, root_path: string): bool
+    if str.length(root_path) > 0
+        return database.store_scripts_rooted(name, extract_dir, root_path)
+    end if
+    return database.store_scripts(name, extract_dir)
+end store_scripts_rooted
+
+// Store config files using rooted version if root is set
+fn store_config_files_rooted(name: string, extract_dir: string, root_path: string): bool
+    if str.length(root_path) > 0
+        return database.store_config_files_rooted(name, extract_dir, root_path)
+    end if
+    return database.store_config_files(name, extract_dir)
+end store_config_files_rooted
+
+// Find package file - either direct path, repository, or search in packages directory
+fn resolve_package_path(pkg_arg: string, cfg: config.Config): string
+    // If it's a file path that exists, use it directly
+    if file.fileExists(pkg_arg)
+        return pkg_arg
+    end if
+
+    // Search in local packages cache first
+    let packages_dir = config.get_packages_dir(cfg)
+
+    // Find all matching packages and select highest version
+    mut entries: [string] = new [string](256)
+    let entry_count = dir.list_dir(packages_dir, entries, 256)
+
+    // Collect all matching package files
+    mut best_file = ""
+    mut best_version = ""
+    mut best_release = 0
+
+    mut i = 0
+    while i < entry_count
+        let entry = entries[i]
+        // Check if entry starts with package name and has correct suffix
+        if str.starts_with(entry, pkg_arg + "-") and str.ends_with(entry, ".pkg.tar.xz")
+            // Extract version from filename
+            let entry_version = ver.extract_version_from_filename(entry, pkg_arg)
+            let entry_release = ver.extract_release_from_filename(entry)
+
+            // Compare with current best
+            if str.length(best_file) == 0
+                // First match
+                best_file = entry
+                best_version = entry_version
+                best_release = entry_release
+            else
+                // Compare versions
+                let cmp = ver.compare_versions(entry_version, best_version)
+                if cmp > 0
+                    // Entry has higher version
+                    best_file = entry
+                    best_version = entry_version
+                    best_release = entry_release
+                elif cmp == 0 and entry_release > best_release
+                    // Same version but higher release
+                    best_file = entry
+                    best_version = entry_version
+                    best_release = entry_release
+                end if
+            end if
+        end if
+        i = i + 1
+    end while
+
+    // Return the best matching package
+    if str.length(best_file) > 0
+        return path.join_path(packages_dir, best_file)
+    end if
+
+    // Also try with .pkg.tar.xz appended
+    let direct_path = path.join_path(packages_dir, pkg_arg + ".pkg.tar.xz")
+    if file.fileExists(direct_path)
+        return direct_path
+    end if
+
+    // Try to build from source if configured
+    if config.get_fallback_to_source(cfg)
+        let port_result = port.find(pkg_arg)
+        if port_result.success
+            color.print_info("No binary package found for " + pkg_arg + ", building from source...")
+            let build_cmd = "coral build -y " + pkg_arg
+            let build_pid = process.process_spawn_shell(build_cmd)
+            if build_pid > 0
+                let build_exit = process.process_wait(build_pid)
+                if build_exit == 0
+                    // Build succeeded, look for the package it produced
+                    let p = port_result.port
+                    let built_pkg = p.info.name + "-" + p.info.version + "-" + int_to_str(p.info.release) + ".pkg.tar.xz"
+                    let built_path = path.join_path(packages_dir, built_pkg)
+                    if file.fileExists(built_path)
+                        return built_path
+                    end if
+                end if
+            end if
+            color.print_warning("Source build failed for " + pkg_arg + ", trying repositories...")
+        end if
+    end if
+
+    // Try to find in repositories and download
+    let remote_result = repository.find_remote_package(pkg_arg)
+    if remote_result.success
+        let remote_pkg = remote_result.pkg
+        color.print_info("Found " + pkg_arg + " " + remote_pkg.version + " in repository: " + remote_pkg.repo_name)
+
+        // Download the package
+        let downloaded_path = repository.download_package(remote_pkg)
+        if str.length(downloaded_path) > 0
+            // Verify checksum if available
+            if str.length(remote_pkg.sha256) > 0
+                color.print_info("Verifying checksum...")
+                if not checksum.verify_checksum(downloaded_path, remote_pkg.sha256)
+                    color.print_error("Checksum verification failed!")
+                    return ""
+                end if
+                color.print_success("Checksum verified")
+            end if
+
+            // Verify signature if required or available
+            let require_signed = config.get_require_signed_packages(cfg)
+            if require_signed or str.length(remote_pkg.signature) > 0
+                let sig_path = downloaded_path + ".sig"
+
+                // Download signature file if we don't have it
+                if not file.fileExists(sig_path) and str.length(remote_pkg.signature) > 0
+                    color.print_info("Downloading signature...")
+                    let sig_url = get_sig_url(remote_pkg)
+                    if not download_signature(sig_url, sig_path)
+                        if require_signed
+                            color.print_error("Failed to download package signature")
+                            return ""
+                        else
+                            color.print_warning("Could not download signature, skipping verification")
+                        end if
+                    end if
+                end if
+
+                // Verify signature if we have it
+                if file.fileExists(sig_path)
+                    color.print_info("Verifying signature...")
+                    let verify_result = signing.verify_package(downloaded_path, sig_path)
+                    if verify_result.success and verify_result.valid
+                        color.print_success("Signature verified (key: " + verify_result.keyid + ")")
+                    else
+                        if require_signed
+                            color.print_error("Signature verification failed: " + verify_result.error)
+                            return ""
+                        else
+                            color.print_warning("Signature verification failed: " + verify_result.error)
+                        end if
+                    end if
+                elif require_signed
+                    color.print_error("Package signature required but not available")
+                    return ""
+                end if
+            end if
+
+            return downloaded_path
+        end if
+    end if
+
+    return ""
+end resolve_package_path
+
+// Create a temporary extraction directory
+fn create_extract_dir(cfg: config.Config): string
+    let work_dir = config.get_work_dir(cfg)
+    let pid = process.getpid()
+    let extract_dir = path.join_path(work_dir, "install_" + int_to_str(pid))
+
+    if dir.dir_exists(extract_dir)
+        // Clean existing
+        let cmd = "rm -rf \"" + extract_dir + "\""
+        let rm_pid = process.process_spawn_shell(cmd)
+        if rm_pid > 0
+            process.process_wait(rm_pid)
+        end if
+    end if
+
+    if not dir.create_dir_all(extract_dir)
+        return ""
+    end if
+
+    return extract_dir
+end create_extract_dir
+
+// Cleanup extraction directory
+fn cleanup_extract_dir(dir_path: string): bool
+    if str.length(dir_path) == 0
+        return true
+    end if
+
+    let cmd = "rm -rf \"" + dir_path + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return false
+    end if
+    process.process_wait(pid)
+    return true
+end cleanup_extract_dir
+
+// Get installation root from config (can be overridden with --root)
+fn get_root(cfg: config.Config): string
+    let root = config.get_root(cfg)
+    if str.length(root) == 0
+        return "/"
+    end if
+    return root
+end get_root
+
+// Check for file conflicts with other installed packages
+// TODO: Implement efficient conflict detection using a file database
+fn check_conflicts(files: [string], file_count: int, pkg_name: string): bool
+    // Skip conflict checking for performance - file overwrites are handled
+    // by the manifest-driven installer and dependencies are managed separately
+    return true
+end check_conflicts
+
+// Helper to create InstalledPackage
+fn new_installed_pkg(info: types.PackageInfo, files: [string], file_count: int): types.InstalledPackage
+    mut pkg = types.new_installed_package()
+    pkg.info = info
+    pkg.install_date = get_date_string()
+    pkg.install_reason = "explicit"
+    pkg.files = files
+    pkg.files_count = file_count
+    return pkg
+end new_installed_pkg
+
+// Get current date as string (simplified)
+fn get_date_string(): string
+    // Use date command for simplicity
+    // TODO: Use proper time functions when available
+    return "2025-01-24"
+end get_date_string
+
+// Get installed package info (root-aware)
+fn get_old_installed(name: string, root_path: string): types.InstalledPackage
+    if str.length(root_path) > 0
+        return database.get_installed_rooted(name, root_path)
+    end if
+    return database.get_installed(name)
+end get_old_installed
+
+// Get old file list for a package (root-aware)
+fn get_old_files(name: string, files: [string], max_count: int, root_path: string): int
+    if str.length(root_path) > 0
+        return database.get_files_rooted(name, files, max_count, root_path)
+    end if
+    return database.get_files(name, files, max_count)
+end get_old_files
+
+// Load old manifest from database for upgrade config protection
+fn load_old_manifest(name: string, root_path: string, entries: [mtree.ManifestEntry], max_count: int): int
+    let manifest_path = database.get_manifest_path(name)
+    if not file.fileExists(manifest_path)
+        return 0
+    end if
+
+    let content = file.readFile(manifest_path)
+    if str.length(content) == 0
+        return 0
+    end if
+
+    return mtree.parse_manifest(content, entries, max_count)
+end load_old_manifest
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+// Build signature URL from remote package info
+fn get_sig_url(pkg: repository.RemotePackage): string
+    mut base_url = pkg.repo_url
+    if not str.ends_with(base_url, "/")
+        base_url = base_url + "/"
+    end if
+
+    // If signature field contains a full URL, use it
+    if str.starts_with(pkg.signature, "http")
+        return pkg.signature
+    end if
+
+    // Otherwise construct from filename
+    return base_url + "packages/" + pkg.filename + ".sig"
+end get_sig_url
+
+// Download a signature file
+fn download_signature(url: string, dest: string): bool
+    return uhttp.download(url, dest)
+end download_signature
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/key.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/key.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,483 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: key.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Key command - manage signing keys
+
+******************************************************************************/
+
+module commands.key
+
+import sys.args
+import sys.process
+import io.file
+import io.dir
+import core.str
+import core.signing
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(): int
+end export
+
+fn execute(): int
+    let argc = args.count()
+
+    if argc < 3
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    let subcommand = args.get(2)
+
+    if subcommand == "list"
+        list_keys()
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "generate"
+        if argc < 4
+            color.print_error("Missing key name")
+            println("Usage: coral key generate <name>")
+            return ec.EXIT_USAGE()
+        end if
+        generate_key(args.get(3))
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "import"
+        if argc < 4
+            color.print_error("Missing key file")
+            println("Usage: coral key import <file> [name]")
+            return ec.EXIT_USAGE()
+        end if
+        let key_file = args.get(3)
+        mut name = ""
+        if argc >= 5
+            name = args.get(4)
+        end if
+        import_key(key_file, name)
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "export"
+        if argc < 4
+            color.print_error("Missing key name")
+            println("Usage: coral key export <name>")
+            return ec.EXIT_USAGE()
+        end if
+        export_key(args.get(3))
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "delete"
+        if argc < 4
+            color.print_error("Missing key name")
+            println("Usage: coral key delete <name>")
+            return ec.EXIT_USAGE()
+        end if
+        delete_key(args.get(3))
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "init"
+        init_keyring()
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "trust"
+        if argc < 4
+            color.print_error("Missing key name")
+            println("Usage: coral key trust <name>")
+            return ec.EXIT_USAGE()
+        end if
+        trust_key(args.get(3))
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "revoke"
+        if argc < 4
+            color.print_error("Missing key name")
+            println("Usage: coral key revoke <name>")
+            return ec.EXIT_USAGE()
+        end if
+        revoke_key(args.get(3))
+        return ec.EXIT_SUCCESS()
+    else
+        color.print_error("Unknown subcommand: " + subcommand)
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+end execute
+
+proc list_keys()
+    color.print_action("Signing keys:")
+    println("")
+
+    mut keys: [signing.KeyPair] = new [signing.KeyPair](64)
+    let count = signing.list_keys(keys, 64)
+
+    if count == 0
+        color.print_info("No keys found")
+        println("")
+        color.print_info("Generate a key with: coral key generate <name>")
+        return
+    end if
+
+    mut i = 0
+    while i < count
+        let key = keys[i]
+        print("  ")
+        print(key.name)
+        if str.length(key.private_key) > 0
+            print(" [private+public]")
+        else
+            print(" [public only]")
+        end if
+        if key.trusted
+            print(" [trusted]")
+        end if
+        println("")
+        i = i + 1
+    end while
+
+    println("")
+    color.print_info("Total: " + int_to_str(count) + " key(s)")
+end list_keys
+
+proc generate_key(name: string)
+    color.print_action("Generating Ed25519 keypair: " + name)
+
+    let result = signing.generate_keypair(name)
+
+    if result.success
+        color.print_success("Keypair generated successfully")
+        println("")
+        color.print_info("Private key: " + signing.get_keyring_dir() + "/" + name + ".key")
+        color.print_info("Public key:  " + signing.get_keyring_dir() + "/" + name + ".pub")
+    else
+        color.print_error(result.error)
+    end if
+end generate_key
+
+proc import_key(key_file: string, name: string)
+    if not file.fileExists(key_file)
+        color.print_error("Key file not found: " + key_file)
+        return
+    end if
+
+    // Determine name from filename if not provided
+    mut key_name = name
+    if str.length(key_name) == 0
+        key_name = extract_name(key_file)
+    end if
+
+    if str.length(key_name) == 0
+        color.print_error("Could not determine key name. Please specify one.")
+        return
+    end if
+
+    color.print_action("Importing key: " + key_name)
+
+    let key_data = file.readFile(key_file)
+    let result = signing.import_key(key_data, key_name)
+
+    if result.success
+        color.print_success("Key imported successfully")
+    else
+        color.print_error(result.error)
+    end if
+end import_key
+
+proc export_key(name: string)
+    let keyring = signing.get_keyring_dir()
+    let pub_path = keyring + "/" + name + ".pub"
+
+    if not file.fileExists(pub_path)
+        color.print_error("Key not found: " + name)
+        return
+    end if
+
+    color.print_action("Public key for " + name + ":")
+    println("")
+
+    let key_data = file.readFile(pub_path)
+    println(key_data)
+end export_key
+
+proc delete_key(name: string)
+    let keyring = signing.get_keyring_dir()
+
+    // Check if key exists
+    let pub_path = keyring + "/" + name + ".pub"
+    let priv_path = keyring + "/" + name + ".key"
+
+    if not file.fileExists(pub_path) and not file.fileExists(priv_path)
+        color.print_error("Key not found: " + name)
+        return
+    end if
+
+    // Ask for confirmation if private key exists
+    if file.fileExists(priv_path)
+        color.print_warning("This will delete the private key! Are you sure?")
+        color.print_info("Use --force to confirm deletion")
+
+        if not args.has_flag("force")
+            return
+        end if
+    end if
+
+    // Delete files
+    if file.fileExists(pub_path)
+        let cmd1 = "rm -f \"" + pub_path + "\""
+        let pid1 = process.process_spawn_shell(cmd1)
+        if pid1 > 0
+            process.process_wait(pid1)
+        end if
+    end if
+
+    if file.fileExists(priv_path)
+        let cmd2 = "rm -f \"" + priv_path + "\""
+        let pid2 = process.process_spawn_shell(cmd2)
+        if pid2 > 0
+            process.process_wait(pid2)
+        end if
+    end if
+
+    color.print_success("Deleted key: " + name)
+end delete_key
+
+fn extract_name(path: string): string
+    // Extract filename without extension
+    let len = str.length(path)
+    if len == 0
+        return ""
+    end if
+
+    // Find last slash
+    mut start = 0
+    mut i = 0
+    while i < len
+        if path[i] == '/'
+            start = i + 1
+        end if
+        i = i + 1
+    end while
+
+    // Find last dot
+    mut end_pos = len
+    i = len - 1
+    while i >= start
+        if path[i] == '.'
+            end_pos = i
+        end if
+        i = i - 1
+    end while
+
+    if end_pos <= start
+        return str.substring(path, start, len - start)
+    end if
+
+    return str.substring(path, start, end_pos - start)
+end extract_name
+
+// Initialize the keyring directory structure
+proc init_keyring()
+    color.print_action("Initializing keyring...")
+
+    let keyring = signing.get_keyring_dir()
+    let trusted_keys_dir = "/var/lib/coral/trusted-keys"
+
+    // Create keyring directory
+    if not dir.dir_exists(keyring)
+        if not dir.create_dir_all(keyring)
+            color.print_error("Failed to create keyring directory: " + keyring)
+            return
+        end if
+        color.print_info("Created: " + keyring)
+    else
+        color.print_info("Keyring already exists: " + keyring)
+    end if
+
+    // Create trusted-keys directory
+    if not dir.dir_exists(trusted_keys_dir)
+        if not dir.create_dir_all(trusted_keys_dir)
+            color.print_error("Failed to create trusted-keys directory")
+            return
+        end if
+        color.print_info("Created: " + trusted_keys_dir)
+    else
+        color.print_info("Trusted keys directory already exists: " + trusted_keys_dir)
+    end if
+
+    // Set permissions (keyring should be root-only for private keys)
+    let cmd = "chmod 700 \"" + keyring + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid > 0
+        process.process_wait(pid)
+    end if
+
+    color.print_success("Keyring initialized")
+    color.print_info("Generate keys with: coral key generate <name>")
+end init_keyring
+
+// Add a key to the trusted keys list
+proc trust_key(name: string)
+    let keyring = signing.get_keyring_dir()
+    let trusted_dir = "/var/lib/coral/trusted-keys"
+    let pub_path = keyring + "/" + name + ".pub"
+
+    // Check if key exists
+    if not file.fileExists(pub_path)
+        color.print_error("Key not found: " + name)
+        color.print_info("Import or generate the key first")
+        return
+    end if
+
+    // Ensure trusted-keys directory exists
+    if not dir.dir_exists(trusted_dir)
+        if not dir.create_dir_all(trusted_dir)
+            color.print_error("Failed to create trusted-keys directory")
+            return
+        end if
+    end if
+
+    // Check if already trusted
+    let trusted_path = trusted_dir + "/" + name + ".pub"
+    if file.fileExists(trusted_path)
+        color.print_warning("Key already trusted: " + name)
+        return
+    end if
+
+    // Copy public key to trusted directory
+    let cmd = "cp \"" + pub_path + "\" \"" + trusted_path + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        color.print_error("Failed to copy key")
+        return
+    end if
+
+    let exit_code = process.process_wait(pid)
+    if exit_code != 0
+        color.print_error("Failed to trust key")
+        return
+    end if
+
+    color.print_success("Trusted key: " + name)
+    color.print_info("Packages signed with this key will now be accepted")
+end trust_key
+
+// Remove a key from the trusted keys list
+proc revoke_key(name: string)
+    let trusted_dir = "/var/lib/coral/trusted-keys"
+    let trusted_path = trusted_dir + "/" + name + ".pub"
+
+    // Check if key is trusted
+    if not file.fileExists(trusted_path)
+        color.print_error("Key is not in trusted list: " + name)
+        return
+    end if
+
+    // Confirm revocation
+    if not args.has_flag("force") and not args.has_flag("yes")
+        color.print_warning("This will revoke trust for key: " + name)
+        color.print_info("Use --force or -y to confirm")
+        return
+    end if
+
+    // Remove from trusted keys
+    let cmd = "rm -f \"" + trusted_path + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        color.print_error("Failed to revoke key")
+        return
+    end if
+
+    let exit_code = process.process_wait(pid)
+    if exit_code != 0
+        color.print_error("Failed to remove key from trusted list")
+        return
+    end if
+
+    color.print_success("Revoked trust for key: " + name)
+    color.print_info("Packages signed with this key will no longer be accepted")
+end revoke_key
+
+// List trusted keys
+proc list_trusted_keys()
+    let trusted_dir = "/var/lib/coral/trusted-keys"
+
+    if not dir.dir_exists(trusted_dir)
+        color.print_info("No trusted keys (directory does not exist)")
+        return
+    end if
+
+    mut entries: [string] = new [string](64)
+    let count = dir.list_dir(trusted_dir, entries, 64)
+
+    if count == 0
+        color.print_info("No trusted keys")
+        return
+    end if
+
+    println("")
+    color.print_action("Trusted keys:")
+    mut i = 0
+    while i < count
+        let entry = entries[i]
+        if str.ends_with(entry, ".pub")
+            let name = str.substring(entry, 0, str.length(entry) - 4)
+            println("  " + name)
+        end if
+        i = i + 1
+    end while
+end list_trusted_keys
+
+proc print_usage()
+    println("Usage: coral key <command> [options]")
+    println("")
+    println("Manage signing keys for package verification.")
+    println("")
+    println("Commands:")
+    println("  list                List all keys in keyring")
+    println("  generate <name>     Generate a new Ed25519 keypair")
+    println("  import <file> [name] Import a public key")
+    println("  export <name>       Export a public key")
+    println("  delete <name>       Delete a key (use --force for private keys)")
+    println("")
+    println("Trust management:")
+    println("  init                Initialize keyring directories")
+    println("  trust <name>        Add key to trusted list")
+    println("  revoke <name>       Remove key from trusted list")
+    println("")
+    println("Examples:")
+    println("  coral key init")
+    println("  coral key generate maintainer")
+    println("  coral key import repo.pub community")
+    println("  coral key trust community")
+end print_usage
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/list.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/list.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,206 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: list.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: List installed or available packages
+
+******************************************************************************/
+
+module commands.list
+
+import sys.args
+import core.str
+import core.config
+import core.database
+import core.port
+import core.portindex
+import types
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(opts: types.GlobalOptions): int
+end export
+
+fn execute(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    // Load configuration and apply command-line overrides
+    let base_cfg = config.load()
+    let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix)
+    let root_path = config.get_root(cfg)
+
+    // Show root if set (verbose mode)
+    if opts.verbose and str.length(root_path) > 0
+        color.print_info("Using root: " + root_path)
+    end if
+
+    // Check for --available flag
+    if args.has_flag("available") or args.has_flag("a")
+        list_available()
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Default: list installed packages
+    list_installed(root_path)
+    return ec.EXIT_SUCCESS()
+end execute
+
+proc list_installed(root_path: string)
+    mut packages: [string] = new [string](512)
+    let pkg_count = list_installed_pkgs(packages, 512, root_path)
+
+    if pkg_count == 0
+        color.print_info("No packages installed")
+        return
+    end if
+
+    color.print_action("Installed packages:")
+    println("")
+
+    mut i = 0
+    while i < pkg_count
+        let pkg_name = packages[i]
+        let pkg = get_installed_pkg(pkg_name, root_path)
+
+        // Format: name version
+        print("  ")
+        print(pkg.info.name)
+        print(" ")
+        print(pkg.info.version)
+        print("-")
+        println(int_to_str(pkg.info.release))
+
+        i = i + 1
+    end while
+
+    println("")
+    color.print_info("Total: " + int_to_str(pkg_count) + " packages")
+end list_installed
+
+// Helper functions for rooted operations
+fn list_installed_pkgs(names: [string], max_count: int, root_path: string): int
+    if str.length(root_path) > 0
+        return database.list_installed_rooted(names, max_count, root_path)
+    end if
+    return database.list_installed(names, max_count)
+end list_installed_pkgs
+
+fn get_installed_pkg(name: string, root_path: string): types.InstalledPackage
+    if str.length(root_path) > 0
+        return database.get_installed_rooted(name, root_path)
+    end if
+    return database.get_installed(name)
+end get_installed_pkg
+
+proc list_available()
+    color.print_action("Available ports:")
+    println("")
+
+    mut total = 0
+
+    // Try ports index first (fast path)
+    if portindex.port_index_exists()
+        mut entries: [portindex.PortIndexEntry] = new [portindex.PortIndexEntry](4096)
+        let entry_count = portindex.list_all_ports(entries, 4096)
+
+        // Group by category for display
+        mut current_cat = ""
+        mut i = 0
+        while i < entry_count
+            let entry = entries[i]
+            if not str.equals(entry.category, current_cat)
+                if str.length(current_cat) > 0
+                    println("")
+                end if
+                current_cat = entry.category
+                color.print_info(current_cat + "/")
+            end if
+
+            print("  ")
+            print(entry.name)
+            print(" ")
+            print(entry.version)
+            print(" - ")
+            println(entry.description)
+            total = total + 1
+
+            i = i + 1
+        end while
+    else
+        // Fall back to filesystem scan
+        mut cats: [string] = new [string](32)
+        let cat_count = port.get_categories(cats, 32)
+
+        mut i = 0
+        while i < cat_count
+            let category = cats[i]
+            mut ports: [string] = new [string](256)
+            let port_count = port.list_category(category, ports, 256)
+
+            if port_count > 0
+                color.print_info(category + "/")
+                mut j = 0
+                while j < port_count
+                    let port_name = ports[j]
+                    let result = port.find(port_name)
+                    if result.success
+                        let p = result.port
+                        print("  ")
+                        print(p.info.name)
+                        print(" ")
+                        print(p.info.version)
+                        print(" - ")
+                        println(p.info.description)
+                    end if
+                    j = j + 1
+                    total = total + 1
+                end while
+                println("")
+            end if
+
+            i = i + 1
+        end while
+    end if
+
+    println("")
+    color.print_info("Total: " + int_to_str(total) + " ports")
+end list_available
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/outdated.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/outdated.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,203 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: outdated.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Outdated command - show packages with available updates
+
+******************************************************************************/
+
+module commands.outdated
+
+import sys.args
+import core.str
+import core.database
+import core.port
+import types
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(): int
+end export
+
+fn execute(): int
+    color.print_action("Checking for available updates...")
+    println("")
+
+    // Get list of installed packages
+    mut installed: [string] = new [string](512)
+    let installed_count = database.list_installed(installed, 512)
+
+    if installed_count == 0
+        color.print_info("No packages installed")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Check each package for updates
+    mut outdated_count = 0
+    mut i = 0
+    while i < installed_count
+        let pkg_name = installed[i]
+        let installed_pkg = database.get_installed(pkg_name)
+
+        // Find port
+        let port_result = port.find(pkg_name)
+
+        if port_result.success
+            let p = port_result.port
+
+            // Compare versions
+            let cmp = compare_versions(installed_pkg.info.version, p.info.version)
+
+            if cmp < 0
+                // Outdated
+                if outdated_count == 0
+                    color.print_info("Outdated packages:")
+                    println("")
+                end if
+
+                print("  ")
+                print(pkg_name)
+                print(" ")
+                print(installed_pkg.info.version)
+                print("-")
+                print(int_to_str(installed_pkg.info.release))
+                print(" -> ")
+                print(p.info.version)
+                print("-")
+                println(int_to_str(p.info.release))
+
+                outdated_count = outdated_count + 1
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    if outdated_count == 0
+        color.print_success("All packages are up to date")
+        // Return special code for scripting - no updates available
+        if args.has_flag("check")
+            return ec.EXIT_NO_UPDATES()
+        end if
+        return ec.EXIT_SUCCESS()
+    else
+        println("")
+        color.print_info(int_to_str(outdated_count) + " package(s) can be upgraded")
+        println("")
+        color.print_info("Run 'coral upgrade' to upgrade all packages")
+        color.print_info("Run 'coral upgrade <package>' to upgrade specific packages")
+        // Return special code for scripting - updates available
+        if args.has_flag("check")
+            return ec.EXIT_UPDATE_AVAILABLE()
+        end if
+        return ec.EXIT_SUCCESS()
+    end if
+end execute
+
+// Compare two version strings
+// Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
+fn compare_versions(v1: string, v2: string): int
+    // Split versions into parts
+    mut parts1: [string] = new [string](10)
+    mut parts2: [string] = new [string](10)
+
+    let count1 = str.split(v1, '.', parts1, 10)
+    let count2 = str.split(v2, '.', parts2, 10)
+
+    // Compare each part
+    mut i = 0
+    let max_parts = max_int(count1, count2)
+
+    while i < max_parts
+        mut n1 = 0
+        mut n2 = 0
+
+        if i < count1
+            n1 = parse_int(parts1[i])
+        end if
+
+        if i < count2
+            n2 = parse_int(parts2[i])
+        end if
+
+        if n1 < n2
+            return 0 - 1
+        elif n1 > n2
+            return 1
+        end if
+
+        i = i + 1
+    end while
+
+    return 0
+end compare_versions
+
+fn max_int(a: int, b: int): int
+    if a > b
+        return a
+    end if
+    return b
+end max_int
+
+// Parse integer from string
+fn parse_int(s: string): int
+    let len = str.length(s)
+    if len == 0
+        return 0
+    end if
+
+    mut result = 0
+    mut i = 0
+
+    while i < len
+        let c = s[i]
+        if c >= '0' and c <= '9'
+            result = result * 10 + (c - '0')
+        else
+            // Stop at non-digit
+            return result
+        end if
+        i = i + 1
+    end while
+
+    return result
+end parse_int
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/owner.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/owner.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,97 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: owner.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Owner command - find which package owns a file
+
+******************************************************************************/
+
+module commands.owner
+
+import sys.args
+import core.str
+import core.config
+import core.database
+import core.pkgdb
+import types
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(opts: types.GlobalOptions): int
+end export
+
+fn execute(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    // File path should be at cmd_index + 1
+    if argc <= opts.cmd_index + 1
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    // Load configuration and apply command-line overrides
+    let base_cfg = config.load()
+    let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix)
+    let root_path = config.get_root(cfg)
+
+    // Show root if set (verbose mode)
+    if opts.verbose and str.length(root_path) > 0
+        color.print_info("Using root: " + root_path)
+    end if
+
+    let file_path = args.get(opts.cmd_index + 1)
+
+    // Find owner (use rooted version if root is set)
+    let owner = find_owner(file_path, root_path)
+
+    if str.length(owner) == 0
+        color.print_error("No package owns: " + file_path)
+        return ec.EXIT_PKG_NOT_FOUND()
+    end if
+
+    // Get package info (use rooted version if root is set)
+    let pkg = get_installed_pkg(owner, root_path)
+
+    color.print_action("File: " + file_path)
+    println("")
+    println("Owned by: " + owner + " " + pkg.info.version)
+    return ec.EXIT_SUCCESS()
+end execute
+
+// Helper functions for rooted operations
+// Uses pkgdb for fast indexed lookup with fallback to database scan
+fn find_owner(file_path: string, root_path: string): string
+    return pkgdb.find_file_owner_rooted(file_path, root_path)
+end find_owner
+
+fn get_installed_pkg(name: string, root_path: string): types.InstalledPackage
+    if str.length(root_path) > 0
+        return database.get_installed_rooted(name, root_path)
+    end if
+    return database.get_installed(name)
+end get_installed_pkg
+
+proc print_usage()
+    println("Usage: coral owner <file>")
+    println("")
+    println("Find which package owns a given file.")
+    println("")
+    println("Arguments:")
+    println("  <file>    Path to the file to look up")
+    println("")
+    println("Examples:")
+    println("  coral owner /usr/bin/vim")
+    println("  coral owner /usr/bin/hello")
+end print_usage
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/ports.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/ports.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,241 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: ports.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Ports command - port scaffolding and cache management
+
+******************************************************************************/
+
+module commands.ports
+
+import sys.args
+import io.file
+import io.dir
+import io.path
+import core.str
+import core.config
+import core.portindex
+import types
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(opts: types.GlobalOptions): int
+end export
+
+fn execute(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    if argc <= opts.cmd_index + 1
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    let subcommand = args.get(opts.cmd_index + 1)
+
+    if subcommand == "new"
+        return ports_new(opts)
+    elif subcommand == "cache"
+        return ports_cache(opts)
+    else
+        color.print_error("Unknown subcommand: " + subcommand)
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+end execute
+
+// ============================================================================
+// coral ports new <category/name>
+// ============================================================================
+
+fn ports_new(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    if argc <= opts.cmd_index + 2
+        color.print_error("Missing argument: category/name")
+        println("Usage: coral ports new <category/name>")
+        return ec.EXIT_USAGE()
+    end if
+
+    let arg = args.get(opts.cmd_index + 2)
+
+    // Require slash format
+    let slash_idx = str.index_of(arg, "/")
+    if slash_idx <= 0
+        color.print_error("Invalid format: '" + arg + "'")
+        println("Use category/name format, e.g.: coral ports new hammerhead/my-port")
+        return ec.EXIT_USAGE()
+    end if
+
+    let category = str.substring(arg, 0, slash_idx)
+    let name = str.substring(arg, slash_idx + 1, str.length(arg) - slash_idx - 1)
+
+    if str.length(name) == 0
+        color.print_error("Port name cannot be empty")
+        return ec.EXIT_USAGE()
+    end if
+
+    // Get ports directory
+    let cfg = config.load()
+    let ports_dir = config.get_ports_dir(cfg)
+
+    // Create port directory
+    let port_dir = path.join_path(ports_dir, path.join_path(category, name))
+
+    if dir.dir_exists(port_dir)
+        color.print_error("Port directory already exists: " + port_dir)
+        return ec.EXIT_PKG_ALREADY_INSTALLED()
+    end if
+
+    if not dir.create_dir_all(port_dir)
+        color.print_error("Failed to create directory: " + port_dir)
+        return ec.EXIT_PERMISSION_DENIED()
+    end if
+
+    // Generate package.toml
+    let toml_path = path.join_path(port_dir, "package.toml")
+    let toml_content = "[package]\nname = \"" + name + "\"\nversion = \"0.1.0\"\nrelease = 1\ndescription = \"\"\nurl = \"\"\nlicense = \"\"\nmaintainer = \"\"\narch = \"x86_64\"\n\n[dependencies]\nruntime = []\nbuild = []\n\n[[source]]\nfile = \"\"\nurls = []\nchecksum = \"SKIP\"\n\n[build]\nparallel = true\njobs = 0\n"
+
+    if not file.writeFile(toml_path, toml_content)
+        color.print_error("Failed to write package.toml")
+        return ec.EXIT_PERMISSION_DENIED()
+    end if
+
+    // Generate build.sh
+    let build_path = path.join_path(port_dir, "build.sh")
+    let dollar = "$"
+    let build_content = "#!/bin/sh\n# Build script for " + name + "\nset -e\n\n# cd " + name + "-" + dollar + "{VERSION}\n\nMAKE=gmake\nif [ \"" + dollar + "(uname -s)\" != \"SunOS\" ]; then\n    MAKE=make\nfi\n\n# ./configure --prefix=/usr\n# " + dollar + "MAKE " + dollar + "{MAKEFLAGS}\n# " + dollar + "MAKE DESTDIR=\"" + dollar + "{DESTDIR}\" install\n"
+
+    if not file.writeFile(build_path, build_content)
+        color.print_error("Failed to write build.sh")
+        return ec.EXIT_PERMISSION_DENIED()
+    end if
+
+    color.print_success("Port created: " + category + "/" + name)
+    println("")
+    println("  Directory: " + port_dir)
+    println("  package.toml: " + toml_path)
+    println("  build.sh: " + build_path)
+    println("")
+    color.print_info("Next steps:")
+    println("  1. Edit package.toml with package details")
+    println("  2. Add source URLs and checksums")
+    println("  3. Write the build.sh script")
+    println("  4. Test with: coral build " + category + "/" + name)
+
+    return ec.EXIT_SUCCESS()
+end ports_new
+
+// ============================================================================
+// coral ports cache [status|rebuild]
+// ============================================================================
+
+fn ports_cache(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    if argc <= opts.cmd_index + 2
+        // Default to status
+        return cache_status()
+    end if
+
+    let subcmd = args.get(opts.cmd_index + 2)
+
+    if subcmd == "status"
+        return cache_status()
+    elif subcmd == "rebuild"
+        return cache_rebuild()
+    else
+        color.print_error("Unknown cache subcommand: " + subcmd)
+        println("Usage: coral ports cache [status|rebuild]")
+        return ec.EXIT_USAGE()
+    end if
+end ports_cache
+
+fn cache_status(): int
+    color.print_action("Ports index status:")
+    println("")
+
+    if portindex.port_index_exists()
+        let count = portindex.get_port_count()
+        println("  Status: built")
+        println("  Ports indexed: " + int_to_str(count))
+    else
+        println("  Status: not built")
+        color.print_info("Run 'coral ports cache rebuild' to build the index")
+    end if
+
+    return ec.EXIT_SUCCESS()
+end cache_status
+
+fn cache_rebuild(): int
+    color.print_action("Rebuilding ports index...")
+    println("")
+
+    if portindex.rebuild_port_index()
+        let count = portindex.get_port_count()
+        color.print_success("Ports index rebuilt successfully")
+        println("  Ports indexed: " + int_to_str(count))
+        return ec.EXIT_SUCCESS()
+    else
+        color.print_error("Failed to rebuild ports index")
+        return ec.EXIT_DB_ERROR()
+    end if
+end cache_rebuild
+
+// ============================================================================
+// Usage
+// ============================================================================
+
+proc print_usage()
+    println("Usage: coral ports <command> [args]")
+    println("")
+    println("Port development and cache management.")
+    println("")
+    println("Commands:")
+    println("  new <category/name>   Scaffold a new port")
+    println("  cache [status]        Show ports index status")
+    println("  cache rebuild         Rebuild ports index")
+    println("")
+    println("Examples:")
+    println("  coral ports new hammerhead/my-port")
+    println("  coral ports cache status")
+    println("  coral ports cache rebuild")
+end print_usage
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/remove.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/remove.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,468 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: remove.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Remove command - uninstall packages
+
+******************************************************************************/
+
+module commands.remove
+
+import sys.args
+import sys.process
+import io.file
+import io.dir
+import io.path
+import core.str
+import core.config
+import core.database
+import core.pkgdb
+import core.package
+import core.port
+import util.prompt
+import util.mtree
+import util.checksum
+import types
+import util.color
+import fs.stat
+import fs.ops
+import exitcodes as ec
+
+export
+    fn execute(opts: types.GlobalOptions): int
+end export
+
+fn execute(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    // Package arguments should be after the command
+    if argc <= opts.cmd_index + 1
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    // Load configuration and apply command-line overrides
+    let base_cfg = config.load()
+    let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix)
+    let root_path = config.get_root(cfg)
+
+    // Show root if set (verbose mode)
+    if opts.verbose and str.length(root_path) > 0
+        color.print_info("Using root: " + root_path)
+    end if
+
+    // Collect packages to remove
+    mut packages: [string] = new [string](256)
+    mut pkg_count = 0
+
+    mut i = opts.cmd_index + 1
+    while i < argc and pkg_count < 256
+        let pkg_name = args.get(i)
+
+        // Skip flags and their values
+        if pkg_name == "--root" or pkg_name == "--prefix"
+            i = i + 2  // Skip flag and its value
+            continue
+        end if
+        if str.starts_with(pkg_name, "-")
+            i = i + 1
+            continue
+        end if
+
+        // Check if installed (use rooted version if root is set)
+        let installed = check_installed(pkg_name, root_path)
+        if installed
+            packages[pkg_count] = pkg_name
+            pkg_count = pkg_count + 1
+        else
+            color.print_warning(pkg_name + " is not installed")
+        end if
+
+        i = i + 1
+    end while
+
+    if pkg_count == 0
+        color.print_info("No packages to remove")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Check for reverse dependencies (packages that depend on these)
+    mut dependents: [string] = new [string](256)
+    mut dependent_count = 0
+
+    i = 0
+    while i < pkg_count
+        let pkg_name = packages[i]
+        let count = get_reverse_dependencies(pkg_name, dependents, dependent_count, 256)
+        dependent_count = count
+        i = i + 1
+    end while
+
+    // Warn if there are dependents
+    if dependent_count > 0
+        color.print_warning("The following installed packages depend on packages being removed:")
+        i = 0
+        while i < dependent_count
+            println("  - " + dependents[i])
+            i = i + 1
+        end while
+
+        // Require --force to proceed
+        if not opts.force
+            color.print_error("Use --force to remove packages with dependents")
+            return ec.EXIT_DEPS_CONFLICT()
+        end if
+        color.print_warning("Proceeding with removal (--force specified)")
+    end if
+
+    // Ask for confirmation (unless -y flag)
+    if not prompt.confirm_remove(packages, pkg_count, dependents, dependent_count, opts)
+        color.print_info("Removal cancelled")
+        return ec.EXIT_CANCELLED()
+    end if
+
+    // Dry-run: show what would be done and exit
+    if opts.dry_run
+        color.print_info("Dry run — no changes made")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Remove packages
+    mut all_success = true
+    i = 0
+    while i < pkg_count
+        if not remove_package(packages[i], root_path)
+            color.print_error("Failed to remove: " + packages[i])
+            all_success = false
+        end if
+        i = i + 1
+    end while
+
+    if all_success
+        return ec.EXIT_SUCCESS()
+    else
+        return ec.EXIT_REMOVE_FAILED()
+    end if
+end execute
+
+// Check if package is installed, using rooted check if root is set
+fn check_installed(name: string, root_path: string): bool
+    if str.length(root_path) > 0
+        return database.is_installed_rooted(name, root_path)
+    end if
+    return database.is_installed(name)
+end check_installed
+
+proc print_usage()
+    println("Usage: coral remove <package...>")
+    println("")
+    println("Remove one or more installed packages.")
+    println("")
+    println("Arguments:")
+    println("  <package>    Name of installed package to remove")
+    println("")
+    println("Options:")
+    println("  -y, --yes    Don't ask for confirmation")
+    println("  -f, --force  Remove even if other packages depend on it")
+    println("")
+    println("Examples:")
+    println("  coral remove vim")
+    println("  coral remove vim nano htop")
+    println("  coral remove --force libfoo")
+end print_usage
+
+fn remove_package(pkg_name: string, root_path: string): bool
+    // Check if package is installed (use rooted version if root is set)
+    if not check_installed(pkg_name, root_path)
+        color.print_error(pkg_name + " is not installed")
+        return false
+    end if
+
+    color.print_action("Removing " + pkg_name + "...")
+
+    // Get package info (use rooted version if root is set)
+    let pkg = get_installed_pkg(pkg_name, root_path)
+    color.print_info("Package: " + pkg.info.name + " " + pkg.info.version)
+
+    // Get file list from database (use rooted version if root is set)
+    mut files: [string] = new [string](4096)
+    let file_count = get_files_list(pkg_name, files, 4096, root_path)
+
+    if file_count == 0
+        color.print_warning("No files recorded for package")
+    else
+        color.print_info("Removing " + int_to_str(file_count) + " files...")
+    end if
+
+    // Run pre-remove script if present (abort on failure)
+    let scripts_dir = database.get_scripts_dir(pkg_name)
+    if database.has_stored_scripts(pkg_name)
+        color.print_info("Running pre-remove script...")
+        if not package.run_stored_pre_remove(scripts_dir, pkg.info.version)
+            color.print_error("Pre-remove script failed, aborting removal of " + pkg_name)
+            return false
+        end if
+    end if
+
+    // Load config file list and manifest checksums for config protection
+    mut config_files: [string] = new [string](256)
+    let config_count = database.get_config_files(pkg_name, config_files, 256)
+
+    mut manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192)
+    mut manifest_count = 0
+    let manifest_path = database.get_manifest_path(pkg_name)
+    if file.fileExists(manifest_path)
+        let manifest_content = file.readFile(manifest_path)
+        if str.length(manifest_content) > 0
+            manifest_count = mtree.parse_manifest(manifest_content, manifest_entries, 8192)
+        end if
+    end if
+
+    // Remove files (with proper root path, respecting config protection)
+    let actual_root = get_actual_root(root_path)
+    if not remove_files(files, file_count, actual_root, config_files, config_count, manifest_entries, manifest_count)
+        color.print_warning("Some files could not be removed")
+        // Continue anyway - unregister from database
+    end if
+
+    // Run post-remove script if present (warn on failure, don't abort)
+    if database.has_stored_scripts(pkg_name)
+        color.print_info("Running post-remove script...")
+        if not package.run_stored_post_remove(scripts_dir, pkg.info.version)
+            color.print_warning("Post-remove script failed")
+        end if
+    end if
+
+    // Unregister from database (use rooted version if root is set)
+    color.print_info("Unregistering package...")
+    if not unregister_pkg(pkg_name, root_path)
+        color.print_error("Failed to unregister package from database")
+        return false
+    end if
+
+    // Update package database indexes
+    if not pkgdb.rebuild_indexes_rooted(root_path)
+        color.print_warning("Failed to update package indexes")
+    end if
+
+    color.print_success("Removed " + pkg_name)
+    return true
+end remove_package
+
+// Get installed package info, using rooted check if root is set
+fn get_installed_pkg(name: string, root_path: string): types.InstalledPackage
+    if str.length(root_path) > 0
+        return database.get_installed_rooted(name, root_path)
+    end if
+    return database.get_installed(name)
+end get_installed_pkg
+
+// Get files list, using rooted check if root is set
+fn get_files_list(name: string, files: [string], max_count: int, root_path: string): int
+    if str.length(root_path) > 0
+        return database.get_files_rooted(name, files, max_count, root_path)
+    end if
+    return database.get_files(name, files, max_count)
+end get_files_list
+
+// Unregister package, using rooted version if root is set
+fn unregister_pkg(name: string, root_path: string): bool
+    if str.length(root_path) > 0
+        return database.unregister_package_rooted(name, root_path)
+    end if
+    return database.unregister_package(name)
+end unregister_pkg
+
+// Get actual root path (returns "/" if empty)
+fn get_actual_root(root_path: string): string
+    if str.length(root_path) == 0
+        return "/"
+    end if
+    return root_path
+end get_actual_root
+
+// Remove files from system, respecting config file protection
+fn remove_files(files: [string], file_count: int, root_path: string, config_files: [string], config_count: int, manifest_entries: [mtree.ManifestEntry], manifest_count: int): bool
+    mut all_success = true
+
+    // Remove files in reverse order
+    mut i = file_count - 1
+    while i >= 0
+        let rel_path = files[i]
+        let full_path = path.join_path(root_path, rel_path)
+
+        if stat.is_symlink(full_path) or stat.exists(full_path)
+            // Check if this is a config file that the user has modified
+            if config_count > 0 and package.is_config_file(rel_path, config_files, config_count)
+                // Look up the package's sha256 from the manifest
+                let pkg_sha = find_manifest_sha256(rel_path, manifest_entries, manifest_count)
+                if str.length(pkg_sha) > 0
+                    let installed_sha = checksum.sha256_file(full_path)
+                    if not str.equals(installed_sha, pkg_sha)
+                        // User has modified this config — preserve it
+                        color.print_info("Preserving modified config: " + rel_path)
+                        i = i - 1
+                        continue
+                    end if
+                end if
+            end if
+
+            if not ops.remove_file(full_path)
+                color.print_warning("Failed to remove: " + full_path)
+                all_success = false
+            end if
+        end if
+
+        i = i - 1
+    end while
+
+    // Try to remove empty parent directories
+    i = file_count - 1
+    while i >= 0
+        let rel_path = files[i]
+        let full_path = path.join_path(root_path, rel_path)
+        let parent = path.dirname(full_path)
+
+        if str.length(parent) > str.length(root_path) and dir.dir_exists(parent)
+            dir.remove_dir(parent)
+        end if
+
+        i = i - 1
+    end while
+
+    return all_success
+end remove_files
+
+// Look up the sha256 for a given relative path in the manifest entries
+fn find_manifest_sha256(rel_path: string, entries: [mtree.ManifestEntry], count: int): string
+    mut i = 0
+    while i < count
+        let epath = mtree.entry_path(entries[i])
+        // Compare with and without ./ prefix
+        mut entry_rel = epath
+        if str.starts_with(epath, "./")
+            entry_rel = str.substring(epath, 2, str.length(epath) - 2)
+        end if
+        if str.equals(entry_rel, rel_path)
+            return mtree.entry_sha256(entries[i])
+        end if
+        i = i + 1
+    end while
+    return ""
+end find_manifest_sha256
+
+// Get packages that depend on the given package
+// Returns new total count (existing + new dependents)
+fn get_reverse_dependencies(pkg_name: string, dependents: [string], current_count: int, max_count: int): int
+    mut count = current_count
+
+    // List all installed packages
+    mut installed: [string] = new [string](512)
+    let installed_count = database.list_installed(installed, 512)
+
+    mut i = 0
+    while i < installed_count and count < max_count
+        let other_pkg = installed[i]
+
+        // Skip the package being removed and already listed dependents
+        if str.equals(other_pkg, pkg_name)
+            i = i + 1
+            continue
+        end if
+
+        // Check if already in dependents list
+        if array_contains(dependents, count, other_pkg)
+            i = i + 1
+            continue
+        end if
+
+        // Try to find this package's port to get its dependencies
+        let port_result = port.find(other_pkg)
+        if port_result.success
+            let p = port_result.port
+
+            // Check runtime dependencies
+            if depends_on(p.deps.runtime, p.deps.runtime_count, pkg_name)
+                dependents[count] = other_pkg
+                count = count + 1
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    return count
+end get_reverse_dependencies
+
+// Check if a dependency list contains the given package
+fn depends_on(deps: [string], dep_count: int, pkg_name: string): bool
+    mut i = 0
+    while i < dep_count
+        let dep = deps[i]
+
+        // Handle alternative dependencies (format: base:opt1,opt2)
+        let colon_pos = str.index_of_char(dep, ':')
+        mut base_dep = dep
+        if colon_pos > 0
+            base_dep = str.substring(dep, 0, colon_pos)
+        end if
+
+        if str.equals(base_dep, pkg_name)
+            return true
+        end if
+
+        i = i + 1
+    end while
+
+    return false
+end depends_on
+
+// Helper to check if array contains a string
+fn array_contains(arr: [string], count: int, s: string): bool
+    mut i = 0
+    while i < count
+        if str.equals(arr[i], s)
+            return true
+        end if
+        i = i + 1
+    end while
+    return false
+end array_contains
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/repo.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/repo.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,670 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: repo.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Repo command - manage package repositories
+
+******************************************************************************/
+
+module commands.repo
+
+import sys.args
+import sys.process
+import io.file
+import io.dir
+import core.str
+import core.repository
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(): int
+end export
+
+fn execute(): int
+    let argc = args.count()
+
+    if argc < 3
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    let subcommand = args.get(2)
+
+    if subcommand == "list"
+        list_repos()
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "add"
+        if argc < 4
+            color.print_error("Missing repository URL")
+            println("Usage: coral repo add <url> [name]")
+            return ec.EXIT_USAGE()
+        end if
+        let url = args.get(3)
+        mut name = ""
+        if argc >= 5
+            name = args.get(4)
+        end if
+        add_repo(url, name)
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "remove"
+        if argc < 4
+            color.print_error("Missing repository name")
+            println("Usage: coral repo remove <name>")
+            return ec.EXIT_USAGE()
+        end if
+        remove_repo(args.get(3))
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "enable"
+        if argc < 4
+            color.print_error("Missing repository name")
+            return ec.EXIT_USAGE()
+        end if
+        enable_repo(args.get(3), true)
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "disable"
+        if argc < 4
+            color.print_error("Missing repository name")
+            return ec.EXIT_USAGE()
+        end if
+        enable_repo(args.get(3), false)
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "init"
+        if argc < 4
+            color.print_error("Missing repository path")
+            println("Usage: coral repo init <path>")
+            return ec.EXIT_USAGE()
+        end if
+        init_repo(args.get(3))
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "rebuild"
+        if argc < 4
+            color.print_error("Missing repository path")
+            println("Usage: coral repo rebuild <path>")
+            return ec.EXIT_USAGE()
+        end if
+        rebuild_repo(args.get(3))
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "sign"
+        if argc < 4
+            color.print_error("Missing repository path")
+            println("Usage: coral repo sign <path> [keyname]")
+            return ec.EXIT_USAGE()
+        end if
+        let repo_path = args.get(3)
+        mut key_name = "default"
+        if argc >= 5
+            key_name = args.get(4)
+        end if
+        sign_repo(repo_path, key_name)
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "verify"
+        if argc < 4
+            color.print_error("Missing repository path")
+            println("Usage: coral repo verify <path>")
+            return ec.EXIT_USAGE()
+        end if
+        verify_repo(args.get(3))
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "mirror"
+        if argc < 5
+            color.print_error("Missing source or destination")
+            println("Usage: coral repo mirror <source> <dest>")
+            return ec.EXIT_USAGE()
+        end if
+        mirror_repo(args.get(3), args.get(4))
+        return ec.EXIT_SUCCESS()
+    elif subcommand == "export"
+        if argc < 5
+            color.print_error("Missing repository path or output")
+            println("Usage: coral repo export <path> <output>")
+            return ec.EXIT_USAGE()
+        end if
+        export_repo(args.get(3), args.get(4))
+        return ec.EXIT_SUCCESS()
+    else
+        color.print_error("Unknown subcommand: " + subcommand)
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+end execute
+
+proc list_repos()
+    color.print_action("Configured repositories:")
+    println("")
+
+    mut repos: [repository.Repository] = new [repository.Repository](32)
+    let count = repository.list_repos(repos, 32)
+
+    if count == 0
+        color.print_info("No repositories configured")
+        println("")
+        color.print_info("Add a repository with: coral repo add <url>")
+        return
+    end if
+
+    mut i = 0
+    while i < count
+        let repo = repos[i]
+        print("  ")
+        print(repo.name)
+        print(" - ")
+        print(repo.url)
+        if not repo.enabled
+            print(" [disabled]")
+        end if
+        if repo.is_signed
+            print(" [signed]")
+        end if
+        println("")
+        i = i + 1
+    end while
+
+    println("")
+    color.print_info("Total: " + int_to_str(count) + " repository(ies)")
+end list_repos
+
+proc add_repo(url: string, name: string)
+    color.print_action("Adding repository...")
+
+    // Generate name from URL if not provided
+    mut repo_name = name
+    if str.length(repo_name) == 0
+        repo_name = extract_repo_name(url)
+    end if
+
+    if str.length(repo_name) == 0
+        color.print_error("Could not determine repository name. Please specify one.")
+        return
+    end if
+
+    // Create repo file
+    let repos_dir = "/etc/coral/repos.d"
+    let repo_path = repos_dir + "/" + repo_name + ".repo"
+
+    // Check if it exists
+    if file.fileExists(repo_path)
+        color.print_error("Repository already exists: " + repo_name)
+        return
+    end if
+
+    // Ensure directory exists
+    if not dir.dir_exists(repos_dir)
+        if not dir.create_dir_all(repos_dir)
+            color.print_error("Failed to create repos directory")
+            return
+        end if
+    end if
+
+    // Create repo file content
+    mut content = "[repository]\n"
+    content = content + "name = \"" + repo_name + "\"\n"
+    content = content + "url = \"" + url + "\"\n"
+    content = content + "priority = 100\n"
+    content = content + "enabled = true\n"
+    content = content + "signed = false\n"
+
+    if not file.writeFile(repo_path, content)
+        color.print_error("Failed to write repository file")
+        return
+    end if
+
+    color.print_success("Added repository: " + repo_name)
+end add_repo
+
+proc remove_repo(name: string)
+    let repos_dir = "/etc/coral/repos.d"
+    let repo_path = repos_dir + "/" + name + ".repo"
+
+    if not file.fileExists(repo_path)
+        color.print_error("Repository not found: " + name)
+        return
+    end if
+
+    // Remove the file
+    let cmd = "rm -f \"" + repo_path + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid > 0
+        process.process_wait(pid)
+    end if
+
+    color.print_success("Removed repository: " + name)
+end remove_repo
+
+proc enable_repo(name: string, enable: bool)
+    let repos_dir = "/etc/coral/repos.d"
+    let repo_path = repos_dir + "/" + name + ".repo"
+
+    if not file.fileExists(repo_path)
+        color.print_error("Repository not found: " + name)
+        return
+    end if
+
+    // Read current content
+    let content = file.readFile(repo_path)
+
+    // Replace enabled line
+    mut new_content = ""
+    if enable
+        new_content = str.replace(content, "enabled = false", "enabled = true")
+    else
+        new_content = str.replace(content, "enabled = true", "enabled = false")
+    end if
+
+    if not file.writeFile(repo_path, new_content)
+        color.print_error("Failed to update repository file")
+        return
+    end if
+
+    if enable
+        color.print_success("Enabled repository: " + name)
+    else
+        color.print_success("Disabled repository: " + name)
+    end if
+end enable_repo
+
+fn extract_repo_name(url: string): string
+    // Extract name from URL (e.g., https://repo.example.com/zygaena -> zygaena)
+    let len = str.length(url)
+    if len == 0
+        return ""
+    end if
+
+    // Find last slash
+    mut last_slash = 0 - 1
+    mut i = 0
+    while i < len
+        if url[i] == '/'
+            last_slash = i
+        end if
+        i = i + 1
+    end while
+
+    if last_slash < 0 or last_slash >= len - 1
+        return "repo"
+    end if
+
+    return str.substring(url, last_slash + 1, len - last_slash - 1)
+end extract_repo_name
+
+// Initialize a new repository at the given path
+proc init_repo(repo_path: string)
+    color.print_action("Initializing repository at " + repo_path)
+
+    // Create the repository directory
+    if not dir.dir_exists(repo_path)
+        if not dir.create_dir_all(repo_path)
+            color.print_error("Failed to create repository directory")
+            return
+        end if
+    end if
+
+    // Create packages subdirectory
+    let packages_dir = repo_path + "/packages"
+    if not dir.dir_exists(packages_dir)
+        if not dir.create_dir_all(packages_dir)
+            color.print_error("Failed to create packages directory")
+            return
+        end if
+    end if
+
+    // Create repo.toml manifest
+    let manifest_path = repo_path + "/repo.toml"
+    if file.fileExists(manifest_path)
+        color.print_warning("repo.toml already exists, skipping")
+    else
+        mut content = "# Coral Repository Manifest\n"
+        content = content + "# Generated by coral repo init\n\n"
+        content = content + "[repository]\n"
+        content = content + "name = \"unnamed\"\n"
+        content = content + "description = \"A Coral package repository\"\n"
+        content = content + "url = \"\"\n"
+        content = content + "arch = \"x86_64\"\n"
+        content = content + "created = \"2025-01-24\"\n"
+        content = content + "signed = false\n\n"
+        content = content + "# Packages will be listed below by coral repo rebuild\n"
+        content = content + "[packages]\n"
+
+        if not file.writeFile(manifest_path, content)
+            color.print_error("Failed to write repo.toml")
+            return
+        end if
+    end if
+
+    color.print_success("Repository initialized at " + repo_path)
+    color.print_info("Place packages in: " + packages_dir)
+    color.print_info("Run 'coral repo rebuild " + repo_path + "' to update manifest")
+end init_repo
+
+// Rebuild repository manifest by scanning packages
+proc rebuild_repo(repo_path: string)
+    color.print_action("Rebuilding repository manifest...")
+
+    let packages_dir = repo_path + "/packages"
+    if not dir.dir_exists(packages_dir)
+        color.print_error("Packages directory not found: " + packages_dir)
+        return
+    end if
+
+    // Scan for package files
+    mut entries: [string] = new [string](1024)
+    let entry_count = dir.list_dir(packages_dir, entries, 1024)
+
+    mut pkg_count = 0
+    mut packages_content = ""
+
+    mut i = 0
+    while i < entry_count
+        let entry = entries[i]
+        // Look for .pkg.tar.xz files
+        if str.ends_with(entry, ".pkg.tar.xz")
+            // Parse package name and version from filename
+            // Format: name-version-release.arch.pkg.tar.xz
+            let info = parse_package_filename(entry)
+            if str.length(info) > 0
+                packages_content = packages_content + info + "\n"
+                pkg_count = pkg_count + 1
+            end if
+        end if
+        i = i + 1
+    end while
+
+    // Generate new repo.toml
+    let manifest_path = repo_path + "/repo.toml"
+    mut content = "# Coral Repository Manifest\n"
+    content = content + "# Rebuilt by coral repo rebuild\n\n"
+    content = content + "[repository]\n"
+    content = content + "name = \"unnamed\"\n"
+    content = content + "description = \"A Coral package repository\"\n"
+    content = content + "url = \"\"\n"
+    content = content + "arch = \"x86_64\"\n"
+    content = content + "created = \"2025-01-24\"\n"
+    content = content + "signed = false\n\n"
+    content = content + "[packages]\n"
+    content = content + packages_content
+
+    if not file.writeFile(manifest_path, content)
+        color.print_error("Failed to write repo.toml")
+        return
+    end if
+
+    color.print_success("Repository rebuilt: " + int_to_str(pkg_count) + " package(s) found")
+end rebuild_repo
+
+// Parse package filename into TOML entry
+fn parse_package_filename(filename: string): string
+    // Extract name-version-release from filename
+    // Example: vim-9.0-1.x86_64.pkg.tar.xz -> vim = { version = "9.0", release = 1 }
+
+    // Remove .pkg.tar.xz suffix
+    let base = str.substring(filename, 0, str.length(filename) - 11)
+
+    // Find arch (e.g., .x86_64)
+    let arch_pos = str.last_index_of_char(base, '.')
+    if arch_pos < 0
+        return ""
+    end if
+
+    let name_ver_rel = str.substring(base, 0, arch_pos)
+
+    // Find release number (last hyphen)
+    let rel_pos = str.last_index_of_char(name_ver_rel, '-')
+    if rel_pos < 0
+        return ""
+    end if
+    let release = str.substring(name_ver_rel, rel_pos + 1, str.length(name_ver_rel) - rel_pos - 1)
+
+    // Find version (second to last hyphen)
+    let name_ver = str.substring(name_ver_rel, 0, rel_pos)
+    let ver_pos = str.last_index_of_char(name_ver, '-')
+    if ver_pos < 0
+        return ""
+    end if
+
+    let name = str.substring(name_ver, 0, ver_pos)
+    let version = str.substring(name_ver, ver_pos + 1, str.length(name_ver) - ver_pos - 1)
+
+    return name + " = { version = \"" + version + "\", release = " + release + ", file = \"" + filename + "\" }"
+end parse_package_filename
+
+// Sign the repository manifest
+proc sign_repo(repo_path: string, key_name: string)
+    color.print_action("Signing repository manifest...")
+
+    let manifest_path = repo_path + "/repo.toml"
+    if not file.fileExists(manifest_path)
+        color.print_error("repo.toml not found in: " + repo_path)
+        return
+    end if
+
+    // Find the key
+    let keyring = "/etc/coral/keys"
+    let key_path = keyring + "/" + key_name + ".key"
+
+    if not file.fileExists(key_path)
+        color.print_error("Key not found: " + key_name)
+        color.print_info("Generate a key with: coral key generate " + key_name)
+        return
+    end if
+
+    // Sign the manifest using openssl
+    let sig_path = repo_path + "/repo.toml.sig"
+    let cmd = "openssl pkeyutl -sign -inkey \"" + key_path + "\" -in \"" + manifest_path + "\" -out \"" + sig_path + "\""
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        color.print_error("Failed to start signing process")
+        return
+    end if
+
+    let exit_code = process.process_wait(pid)
+    if exit_code != 0
+        color.print_error("Signing failed")
+        return
+    end if
+
+    // Update repo.toml to indicate it's signed
+    let content = file.readFile(manifest_path)
+    let new_content = str.replace(content, "signed = false", "signed = true")
+    file.writeFile(manifest_path, new_content)
+
+    color.print_success("Repository signed with key: " + key_name)
+    color.print_info("Signature: " + sig_path)
+end sign_repo
+
+// Verify repository signature
+proc verify_repo(repo_path: string)
+    color.print_action("Verifying repository signature...")
+
+    let manifest_path = repo_path + "/repo.toml"
+    let sig_path = repo_path + "/repo.toml.sig"
+
+    if not file.fileExists(manifest_path)
+        color.print_error("repo.toml not found")
+        return
+    end if
+
+    if not file.fileExists(sig_path)
+        color.print_error("No signature found (repo.toml.sig)")
+        return
+    end if
+
+    // Try to find a matching public key
+    let keyring = "/etc/coral/keys"
+    if not dir.dir_exists(keyring)
+        color.print_error("No keys in keyring")
+        return
+    end if
+
+    // List all public keys and try each
+    mut entries: [string] = new [string](64)
+    let count = dir.list_dir(keyring, entries, 64)
+
+    mut i = 0
+    mut verified = false
+    while i < count and not verified
+        let entry = entries[i]
+        if str.ends_with(entry, ".pub")
+            let key_path = keyring + "/" + entry
+            let cmd = "openssl pkeyutl -verify -pubin -inkey \"" + key_path + "\" -in \"" + manifest_path + "\" -sigfile \"" + sig_path + "\" 2>/dev/null"
+
+            let pid = process.process_spawn_shell(cmd)
+            if pid > 0
+                let exit_code = process.process_wait(pid)
+                if exit_code == 0
+                    let key_name = str.substring(entry, 0, str.length(entry) - 4)
+                    color.print_success("Signature verified with key: " + key_name)
+                    verified = true
+                end if
+            end if
+        end if
+        i = i + 1
+    end while
+
+    if not verified
+        color.print_error("Signature verification failed - no matching key found")
+    end if
+end verify_repo
+
+// Mirror repository to remote destination
+proc mirror_repo(source: string, dest: string)
+    color.print_action("Mirroring repository...")
+    color.print_info("Source: " + source)
+    color.print_info("Destination: " + dest)
+
+    // Use rsync for mirroring
+    let cmd = "rsync -av --delete \"" + source + "/\" \"" + dest + "/\""
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        color.print_error("Failed to start rsync")
+        return
+    end if
+
+    let exit_code = process.process_wait(pid)
+    if exit_code != 0
+        color.print_error("Mirror sync failed")
+        return
+    end if
+
+    color.print_success("Repository mirrored to: " + dest)
+end mirror_repo
+
+// Export repository for offline distribution (ISO/USB)
+proc export_repo(repo_path: string, output_path: string)
+    color.print_action("Exporting repository for offline distribution...")
+
+    if not dir.dir_exists(repo_path)
+        color.print_error("Repository not found: " + repo_path)
+        return
+    end if
+
+    // Determine output format based on extension
+    if str.ends_with(output_path, ".tar.xz")
+        // Create compressed tarball
+        let cmd = "gtar -C \"" + repo_path + "\" -cJf \"" + output_path + "\" ."
+        let pid = process.process_spawn_shell(cmd)
+        if pid < 0
+            color.print_error("Failed to create archive")
+            return
+        end if
+        let exit_code = process.process_wait(pid)
+        if exit_code != 0
+            color.print_error("Archive creation failed")
+            return
+        end if
+    elif str.ends_with(output_path, ".tar.gz")
+        let cmd = "gtar -C \"" + repo_path + "\" -czf \"" + output_path + "\" ."
+        let pid = process.process_spawn_shell(cmd)
+        if pid < 0
+            color.print_error("Failed to create archive")
+            return
+        end if
+        let exit_code = process.process_wait(pid)
+        if exit_code != 0
+            color.print_error("Archive creation failed")
+            return
+        end if
+    else
+        // Copy directory
+        let cmd = "cp -r \"" + repo_path + "\" \"" + output_path + "\""
+        let pid = process.process_spawn_shell(cmd)
+        if pid < 0
+            color.print_error("Failed to copy repository")
+            return
+        end if
+        let exit_code = process.process_wait(pid)
+        if exit_code != 0
+            color.print_error("Copy failed")
+            return
+        end if
+    end if
+
+    color.print_success("Repository exported to: " + output_path)
+end export_repo
+
+proc print_usage()
+    println("Usage: coral repo <command> [options]")
+    println("")
+    println("Manage package repositories.")
+    println("")
+    println("Commands:")
+    println("  list              List configured repositories")
+    println("  add <url> [name]  Add a new repository")
+    println("  remove <name>     Remove a repository")
+    println("  enable <name>     Enable a repository")
+    println("  disable <name>    Disable a repository")
+    println("")
+    println("Repository maintenance:")
+    println("  init <path>       Create a new repository structure")
+    println("  rebuild <path>    Scan packages and rebuild manifest")
+    println("  sign <path> [key] Sign repository with Ed25519 key")
+    println("  verify <path>     Verify repository signature")
+    println("  mirror <src> <dst> Sync repository to mirror (rsync)")
+    println("  export <path> <out> Export for offline distribution")
+    println("")
+    println("Examples:")
+    println("  coral repo list")
+    println("  coral repo add https://repo.zygaena.org/packages")
+    println("  coral repo init /var/www/repo/myrepo")
+    println("  coral repo rebuild /var/www/repo/myrepo")
+    println("  coral repo sign /var/www/repo/myrepo maintainer")
+end print_usage
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/search.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/search.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,192 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: search.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Search command - search for packages by name or description
+
+******************************************************************************/
+
+module commands.search
+
+import sys.args
+import core.str
+import core.port
+import core.portindex
+import core.database
+import types
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(): int
+end export
+
+fn execute(): int
+    let argc = args.count()
+
+    if argc < 3
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    let search_term = args.get(2)
+
+    color.print_action("Searching for: " + search_term)
+    println("")
+
+    mut total = 0
+
+    // Try ports index first (fast path)
+    if portindex.port_index_exists()
+        mut results: [portindex.PortIndexEntry] = new [portindex.PortIndexEntry](512)
+        let result_count = portindex.search_ports(search_term, results, 512)
+
+        mut i = 0
+        while i < result_count
+            let entry = results[i]
+            print_index_match(entry)
+            total = total + 1
+            i = i + 1
+        end while
+    else
+        // Fall back to filesystem scan
+        mut cats: [string] = new [string](32)
+        let cat_count = port.get_categories(cats, 32)
+
+        mut i = 0
+        while i < cat_count
+            let category = cats[i]
+            mut ports: [string] = new [string](256)
+            let port_count = port.list_category(category, ports, 256)
+
+            mut j = 0
+            while j < port_count
+                let port_name = ports[j]
+                let result = port.find(port_name)
+                if result.success
+                    let p = result.port
+
+                    // Check if search term matches name or description
+                    if matches_search(p, search_term)
+                        print_match(p, category)
+                        total = total + 1
+                    end if
+                end if
+                j = j + 1
+            end while
+
+            i = i + 1
+        end while
+    end if
+
+    if total == 0
+        color.print_info("No packages found matching: " + search_term)
+    else
+        println("")
+        color.print_info("Found " + int_to_str(total) + " package(s)")
+    end if
+    return ec.EXIT_SUCCESS()
+end execute
+
+fn matches_search(p: types.Port, term: string): bool
+    let lower_term = str.to_lower(term)
+    let lower_name = str.to_lower(p.info.name)
+    let lower_desc = str.to_lower(p.info.description)
+
+    // Check name
+    if str.contains(lower_name, lower_term)
+        return true
+    end if
+
+    // Check description
+    if str.contains(lower_desc, lower_term)
+        return true
+    end if
+
+    return false
+end matches_search
+
+proc print_index_match(entry: portindex.PortIndexEntry)
+    let is_installed = database.is_installed(entry.name)
+
+    print(entry.category)
+    print("/")
+    print(entry.name)
+    print(" ")
+    print(entry.version)
+    if is_installed
+        print(" [installed]")
+    end if
+    println("")
+    print("    ")
+    println(entry.description)
+end print_index_match
+
+proc print_match(p: types.Port, category: string)
+    // Check if installed
+    let is_installed = database.is_installed(p.info.name)
+
+    print(category)
+    print("/")
+    print(p.info.name)
+    print(" ")
+    print(p.info.version)
+    if is_installed
+        print(" [installed]")
+    end if
+    println("")
+    print("    ")
+    println(p.info.description)
+end print_match
+
+proc print_usage()
+    println("Usage: coral search <term>")
+    println("")
+    println("Search for packages by name or description.")
+    println("")
+    println("Arguments:")
+    println("  <term>    Search term to look for")
+    println("")
+    println("Examples:")
+    println("  coral search vim")
+    println("  coral search editor")
+    println("  coral search hello")
+end print_usage
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/sync.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/sync.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,158 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: sync.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Sync command - update ports collection from repository
+
+******************************************************************************/
+
+module commands.sync
+
+import sys.args
+import sys.process
+import io.dir
+import io.path
+import io.file
+import core.str
+import core.config
+import core.port
+import core.portindex
+import util.color
+import exitcodes as ec
+
+export
+    fn execute(): int
+end export
+
+fn execute(): int
+    // Load configuration
+    let cfg = config.load()
+    let ports_dir = config.get_ports_dir(cfg)
+
+    color.print_action("Syncing ports collection...")
+    println("")
+
+    // Check if ports directory exists
+    if not dir.dir_exists(ports_dir)
+        color.print_error("Ports directory does not exist: " + ports_dir)
+        color.print_info("You may need to clone the ports repository first:")
+        println("  git clone <ports-repo-url> " + ports_dir)
+        return ec.EXIT_CONFIG_ERROR()
+    end if
+
+    // Check if it's a git repository
+    let git_dir = path.join_path(ports_dir, ".git")
+    if not dir.dir_exists(git_dir)
+        color.print_warning("Ports directory is not a git repository")
+        color.print_info("Cannot sync - no version control detected")
+        return ec.EXIT_CONFIG_ERROR()
+    end if
+
+    // Run git pull
+    color.print_info("Updating from remote repository...")
+
+    let cmd = "cd \"" + ports_dir + "\" && git pull"
+    let pid = process.process_spawn_shell(cmd)
+
+    if pid < 0
+        color.print_error("Failed to start git pull")
+        return ec.EXIT_REPO_UNREACHABLE()
+    end if
+
+    let exit_code = process.process_wait(pid)
+
+    if exit_code == 0
+        color.print_success("Ports collection updated successfully")
+
+        // Rebuild ports index after successful sync
+        portindex.rebuild_port_index()
+
+        // Show summary of changes
+        show_update_summary(ports_dir)
+        return ec.EXIT_SUCCESS()
+    else
+        color.print_error("Git pull failed with exit code " + int_to_str(exit_code))
+        color.print_info("Check your network connection and repository access")
+        return ec.EXIT_REPO_UNREACHABLE()
+    end if
+end execute
+
+proc show_update_summary(ports_dir: string)
+    // Count ports in each category (dynamically discovered)
+    mut cats: [string] = new [string](32)
+    let cat_count = port.get_categories(cats, 32)
+
+    mut total = 0
+    mut i = 0
+    while i < cat_count
+        let cat = cats[i]
+        let cat_path = path.join_path(ports_dir, cat)
+
+        if dir.dir_exists(cat_path)
+            mut entries: [string] = new [string](256)
+            let count = dir.list_dir(cat_path, entries, 256)
+
+            // Count valid ports (directories with package.toml)
+            mut port_count = 0
+            mut j = 0
+            while j < count
+                let entry = entries[j]
+                if str.length(entry) > 0 and entry[0] != '.'
+                    let toml_path = path.join_path(path.join_path(cat_path, entry), "package.toml")
+                    if file.fileExists(toml_path)
+                        port_count = port_count + 1
+                    end if
+                end if
+                j = j + 1
+            end while
+
+            if port_count > 0
+                println("  " + cat + ": " + int_to_str(port_count) + " ports")
+                total = total + port_count
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    println("")
+    color.print_info("Total: " + int_to_str(total) + " ports available")
+end show_update_summary
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/upgrade.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/upgrade.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,465 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: upgrade.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Upgrade command - upgrade installed packages
+
+******************************************************************************/
+
+module commands.upgrade
+
+import sys.args
+import sys.process
+import io.file
+import io.dir
+import io.path
+import core.str
+import core.config
+import core.database
+import core.port
+import core.portindex
+import core.package
+import types
+import util.color
+import util.version as ver
+import exitcodes as ec
+
+export
+    fn execute(opts: types.GlobalOptions): int
+end export
+
+fn execute(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    // Load configuration and apply command-line overrides
+    let base_cfg = config.load()
+    let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix)
+    let root_path = config.get_root(cfg)
+
+    // Auto-refresh port index if configured
+    if config.get_auto_refresh(cfg)
+        portindex.rebuild_port_index()
+    end if
+
+    // Show root if set (verbose mode)
+    if opts.verbose and str.length(root_path) > 0
+        color.print_info("Using root: " + root_path)
+    end if
+
+    // Count non-flag arguments (package names)
+    // Skip flag values (--root, --prefix take a value)
+    mut pkg_count = 0
+    mut i = opts.cmd_index + 1
+    while i < argc
+        let arg = args.get(i)
+        if arg == "--root" or arg == "--prefix"
+            i = i + 2  // Skip flag and its value
+            continue
+        end if
+        if not str.starts_with(arg, "-")
+            pkg_count = pkg_count + 1
+        end if
+        i = i + 1
+    end while
+
+    if pkg_count == 0
+        // No packages specified - upgrade all
+        return upgrade_all(cfg)
+    else
+        // Upgrade specified packages
+        mut all_success = true
+        i = opts.cmd_index + 1
+        while i < argc
+            let pkg_name = args.get(i)
+
+            // Skip flags and their values
+            if pkg_name == "--root" or pkg_name == "--prefix"
+                i = i + 2  // Skip flag and its value
+                continue
+            end if
+            if str.starts_with(pkg_name, "-")
+                i = i + 1
+                continue
+            end if
+
+            // Upgrade this package
+            if not upgrade_package(pkg_name, cfg)
+                color.print_error("Failed to upgrade: " + pkg_name)
+                all_success = false
+            end if
+
+            i = i + 1
+        end while
+
+        if all_success
+            return ec.EXIT_SUCCESS()
+        else
+            return ec.EXIT_UPGRADE_FAILED()
+        end if
+    end if
+end execute
+
+fn upgrade_all(cfg: config.Config): int
+    color.print_action("Checking for upgrades...")
+
+    // Get root path for rooted operations
+    let root_path = config.get_root(cfg)
+
+    // List all installed packages (use rooted version if root is set)
+    mut installed: [string] = new [string](512)
+    let installed_count = list_installed_pkgs(installed, 512, root_path)
+
+    if installed_count == 0
+        color.print_info("No packages installed")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Check each package for updates
+    mut upgradable: [string] = new [string](512)
+    mut upgradable_count = 0
+
+    mut i = 0
+    while i < installed_count
+        let pkg_name = installed[i]
+
+        if has_upgrade(pkg_name, root_path)
+            upgradable[upgradable_count] = pkg_name
+            upgradable_count = upgradable_count + 1
+        end if
+
+        i = i + 1
+    end while
+
+    if upgradable_count == 0
+        color.print_success("All packages are up to date")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    color.print_info("Packages to upgrade: " + int_to_str(upgradable_count))
+
+    // List packages to upgrade
+    i = 0
+    while i < upgradable_count
+        let pkg_name = upgradable[i]
+        let installed_pkg = get_installed_pkg(pkg_name, root_path)
+        let port_result = port.find(pkg_name)
+
+        if port_result.success
+            let p = port_result.port
+            println("  " + pkg_name + " " + installed_pkg.info.version + " -> " + p.info.version)
+        end if
+
+        i = i + 1
+    end while
+
+    // Ask for confirmation unless -y flag
+    if not args.has_flag("y") and not args.has_flag("yes")
+        color.print_info("Use -y flag to skip confirmation and proceed with upgrade")
+        return ec.EXIT_CANCELLED()
+    end if
+
+    // Dry-run: show what would be done and exit
+    if args.has_flag("n") or args.has_flag("dry-run")
+        color.print_info("Dry run — no changes made")
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Perform upgrades
+    mut all_success = true
+    i = 0
+    while i < upgradable_count
+        if not upgrade_package(upgradable[i], cfg)
+            color.print_error("Failed to upgrade: " + upgradable[i])
+            all_success = false
+        end if
+        i = i + 1
+    end while
+
+    if all_success
+        return ec.EXIT_SUCCESS()
+    else
+        return ec.EXIT_UPGRADE_FAILED()
+    end if
+end upgrade_all
+
+fn has_upgrade(pkg_name: string, root_path: string): bool
+    // Get installed version (use rooted version if root is set)
+    let installed_pkg = get_installed_pkg(pkg_name, root_path)
+    if str.length(installed_pkg.info.name) == 0
+        return false
+    end if
+
+    // Find port
+    let port_result = port.find(pkg_name)
+    if not port_result.success
+        return false
+    end if
+
+    let p = port_result.port
+
+    // Compare versions using semantic versioning
+    return ver.compare_versions(installed_pkg.info.version, p.info.version) < 0
+end has_upgrade
+
+// Helper functions for rooted operations
+fn list_installed_pkgs(names: [string], max_count: int, root_path: string): int
+    if str.length(root_path) > 0
+        return database.list_installed_rooted(names, max_count, root_path)
+    end if
+    return database.list_installed(names, max_count)
+end list_installed_pkgs
+
+fn get_installed_pkg(name: string, root_path: string): types.InstalledPackage
+    if str.length(root_path) > 0
+        return database.get_installed_rooted(name, root_path)
+    end if
+    return database.get_installed(name)
+end get_installed_pkg
+
+fn is_installed_pkg(name: string, root_path: string): bool
+    if str.length(root_path) > 0
+        return database.is_installed_rooted(name, root_path)
+    end if
+    return database.is_installed(name)
+end is_installed_pkg
+
+fn get_files_list(name: string, files: [string], max_count: int, root_path: string): int
+    if str.length(root_path) > 0
+        return database.get_files_rooted(name, files, max_count, root_path)
+    end if
+    return database.get_files(name, files, max_count)
+end get_files_list
+
+fn unregister_pkg(name: string, root_path: string): bool
+    if str.length(root_path) > 0
+        return database.unregister_package_rooted(name, root_path)
+    end if
+    return database.unregister_package(name)
+end unregister_pkg
+
+fn register_pkg(pkg: types.InstalledPackage, root_path: string): bool
+    if str.length(root_path) > 0
+        return database.register_from_extract_dir_rooted(pkg, "", root_path)
+    end if
+    return database.register_package(pkg)
+end register_pkg
+
+fn get_actual_root(root_path: string): string
+    if str.length(root_path) == 0
+        return "/"
+    end if
+    return root_path
+end get_actual_root
+
+fn upgrade_package(pkg_name: string, cfg: config.Config): bool
+    // Get root path for rooted operations
+    let root_path = config.get_root(cfg)
+
+    // Check if installed (use rooted version if root is set)
+    if not is_installed_pkg(pkg_name, root_path)
+        color.print_error(pkg_name + " is not installed")
+        return false
+    end if
+
+    // Get installed info (use rooted version if root is set)
+    let installed_pkg = get_installed_pkg(pkg_name, root_path)
+
+    // Find port
+    let port_result = port.find(pkg_name)
+    if not port_result.success
+        color.print_error("Port not found: " + pkg_name)
+        return false
+    end if
+
+    let p = port_result.port
+
+    // Check if upgrade is available using semantic versioning
+    let cmp = ver.compare_versions(installed_pkg.info.version, p.info.version)
+    if cmp >= 0
+        color.print_info(pkg_name + " is already at version " + installed_pkg.info.version)
+        return true
+    end if
+
+    color.print_action("Upgrading " + pkg_name + " from " + installed_pkg.info.version + " to " + p.info.version + "...")
+
+    // Look for pre-built package
+    let pkg_path = find_package(pkg_name, p.info.version, p.info.release, cfg)
+
+    if str.length(pkg_path) == 0
+        color.print_error("Package not found. Build it first with: coral build")
+        return false
+    end if
+
+    // Remove old version (keep database entry for now)
+    mut files: [string] = new [string](4096)
+    let file_count = get_files_list(pkg_name, files, 4096, root_path)
+
+    color.print_info("Removing old files...")
+    let actual_root = get_actual_root(root_path)
+    remove_files(files, file_count, actual_root)
+
+    // Install new version
+    color.print_info("Installing new version...")
+
+    // Create extraction directory
+    let extract_dir = create_extract_dir(cfg)
+    if str.length(extract_dir) == 0
+        color.print_error("Failed to create extraction directory")
+        return false
+    end if
+
+    // Extract package
+    let result = package.extract_package(pkg_path, extract_dir)
+    if not result.success
+        color.print_error(result.error)
+        cleanup_extract_dir(extract_dir)
+        return false
+    end if
+
+    // Read new file list
+    mut new_files: [string] = new [string](4096)
+    let new_file_count = package.read_footprint(extract_dir, new_files, 4096)
+
+    // Install files to proper root
+    if not package.install_files(extract_dir, actual_root)
+        color.print_error("Failed to install files")
+        cleanup_extract_dir(extract_dir)
+        return false
+    end if
+
+    // Update database (remove and re-add) using rooted versions if needed
+    unregister_pkg(pkg_name, root_path)
+
+    let new_info = package.read_pkginfo(extract_dir)
+    mut new_pkg = types.new_installed_package()
+    new_pkg.info = new_info
+    new_pkg.install_date = "2025-01-24"
+    new_pkg.install_reason = "explicit"
+    new_pkg.files = new_files
+    new_pkg.files_count = new_file_count
+
+    // Register using rooted version if root is set
+    if str.length(root_path) > 0
+        if not database.register_from_extract_dir_rooted(new_pkg, extract_dir, root_path)
+            color.print_warning("Failed to update database")
+        end if
+    else
+        if not database.register_from_extract_dir(new_pkg, extract_dir)
+            color.print_warning("Failed to update database")
+        end if
+    end if
+
+    // Cleanup
+    cleanup_extract_dir(extract_dir)
+
+    color.print_success("Upgraded " + pkg_name + " to " + p.info.version)
+    return true
+end upgrade_package
+
+// Find a package file by name and version
+fn find_package(name: string, version: string, release: int, cfg: config.Config): string
+    let packages_dir = config.get_packages_dir(cfg)
+    let pkg_name = name + "-" + version + "-" + int_to_str(release) + ".pkg.tar.xz"
+    let pkg_path = path.join_path(packages_dir, pkg_name)
+
+    if file.fileExists(pkg_path)
+        return pkg_path
+    end if
+
+    return ""
+end find_package
+
+// Remove files from system
+fn remove_files(files: [string], file_count: int, root_path: string): bool
+    mut i = file_count - 1
+    while i >= 0
+        let rel_path = files[i]
+        let full_path = path.join_path(root_path, rel_path)
+
+        if file.fileExists(full_path)
+            let cmd = "rm -f \"" + full_path + "\""
+            let pid = process.process_spawn_shell(cmd)
+            if pid > 0
+                process.process_wait(pid)
+            end if
+        end if
+
+        i = i - 1
+    end while
+
+    return true
+end remove_files
+
+// Create extraction directory
+fn create_extract_dir(cfg: config.Config): string
+    let work_dir = config.get_work_dir(cfg)
+    let pid = process.getpid()
+    let extract_dir = path.join_path(work_dir, "upgrade_" + int_to_str(pid))
+
+    if dir.dir_exists(extract_dir)
+        let cmd = "rm -rf \"" + extract_dir + "\""
+        let rm_pid = process.process_spawn_shell(cmd)
+        if rm_pid > 0
+            process.process_wait(rm_pid)
+        end if
+    end if
+
+    if not dir.create_dir_all(extract_dir)
+        return ""
+    end if
+
+    return extract_dir
+end create_extract_dir
+
+// Cleanup extraction directory
+fn cleanup_extract_dir(dir_path: string): bool
+    if str.length(dir_path) == 0
+        return true
+    end if
+
+    let cmd = "rm -rf \"" + dir_path + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return false
+    end if
+    process.process_wait(pid)
+    return true
+end cleanup_extract_dir
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/commands/verify.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/commands/verify.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,315 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: verify.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Verify command - check installed package integrity
+
+******************************************************************************/
+
+module commands.verify
+
+import sys.args
+import io.file
+import io.dir
+import io.path
+import core.str
+import core.config
+import core.database
+import util.mtree
+import util.checksum
+import types
+import util.color
+import exitcodes as ec
+import fs.stat
+import fs.link
+
+export
+    fn execute(opts: types.GlobalOptions): int
+end export
+
+fn execute(opts: types.GlobalOptions): int
+    let argc = args.count()
+
+    if argc <= opts.cmd_index + 1
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    // Load configuration and apply command-line overrides
+    let base_cfg = config.load()
+    let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix)
+    let root_path = config.get_root(cfg)
+
+    if opts.verbose and str.length(root_path) > 0
+        color.print_info("Using root: " + root_path)
+    end if
+
+    let pkg_name = args.get(opts.cmd_index + 1)
+
+    // Check if installed
+    if not check_installed(pkg_name, root_path)
+        color.print_error(pkg_name + " is not installed")
+        return ec.EXIT_PKG_NOT_FOUND()
+    end if
+
+    color.print_action("Verifying " + pkg_name + "...")
+
+    // Load manifest from database
+    let manifest_path = database.get_manifest_path(pkg_name)
+    if not file.fileExists(manifest_path)
+        color.print_error("No manifest found for " + pkg_name + " (legacy package without .MANIFEST)")
+        return ec.EXIT_PKG_INVALID()
+    end if
+
+    let manifest_content = file.readFile(manifest_path)
+    if str.length(manifest_content) == 0
+        color.print_error("Empty manifest for " + pkg_name)
+        return ec.EXIT_PKG_INVALID()
+    end if
+
+    mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192)
+    let entry_count = mtree.parse_manifest(manifest_content, entries, 8192)
+
+    if entry_count == 0
+        color.print_error("Failed to parse manifest for " + pkg_name)
+        return ec.EXIT_PKG_INVALID()
+    end if
+
+    // Determine actual root
+    let actual_root = get_actual_root(root_path)
+
+    // Verify each entry
+    mut missing_count = 0
+    mut checksum_fail_count = 0
+    mut mode_fail_count = 0
+    mut link_fail_count = 0
+    mut ok_count = 0
+
+    mut i = 0
+    while i < entry_count
+        let entry = entries[i]
+        let etype = mtree.entry_type(entry)
+        let epath = mtree.entry_path(entry)
+
+        mut rel_path = epath
+        if str.starts_with(epath, "./")
+            rel_path = str.substring(epath, 2, str.length(epath) - 2)
+        end if
+
+        let full_path = path.join_path(actual_root, rel_path)
+
+        if str.equals(etype, "dir")
+            if not dir.dir_exists(full_path)
+                color.print_warning("MISSING dir:  " + rel_path)
+                missing_count = missing_count + 1
+            else
+                // Check mode
+                let expected_mode = mtree.entry_mode(entry)
+                let actual_mode = stat.file_mode(full_path)
+                // Compare lower 12 bits (permissions + setuid/setgid/sticky)
+                if (actual_mode % 4096) != (expected_mode % 4096)
+                    if opts.verbose
+                        color.print_warning("MODE " + rel_path + " (expected " + int_to_octal(expected_mode) + ", got " + int_to_octal(actual_mode) + ")")
+                    else
+                        color.print_warning("MODE          " + rel_path)
+                    end if
+                    mode_fail_count = mode_fail_count + 1
+                else
+                    ok_count = ok_count + 1
+                end if
+            end if
+
+        elif str.equals(etype, "file")
+            if not stat.exists(full_path)
+                color.print_warning("MISSING file: " + rel_path)
+                missing_count = missing_count + 1
+            else
+                mut file_ok = true
+
+                // Check sha256
+                let expected_sha = mtree.entry_sha256(entry)
+                if str.length(expected_sha) > 0
+                    let actual_sha = checksum.sha256_file(full_path)
+                    if not str.equals(actual_sha, expected_sha)
+                        color.print_warning("CHECKSUM      " + rel_path)
+                        checksum_fail_count = checksum_fail_count + 1
+                        file_ok = false
+                    end if
+                end if
+
+                // Check mode
+                let expected_mode = mtree.entry_mode(entry)
+                let actual_mode = stat.file_mode(full_path)
+                if (actual_mode % 4096) != (expected_mode % 4096)
+                    if opts.verbose
+                        color.print_warning("MODE " + rel_path + " (expected " + int_to_octal(expected_mode) + ", got " + int_to_octal(actual_mode) + ")")
+                    else
+                        color.print_warning("MODE          " + rel_path)
+                    end if
+                    mode_fail_count = mode_fail_count + 1
+                    file_ok = false
+                end if
+
+                // Check ownership
+                let expected_uid = mtree.name_to_uid(mtree.entry_uname(entry))
+                let expected_gid = mtree.name_to_gid(mtree.entry_gname(entry))
+                let actual_uid = stat.file_uid(full_path)
+                let actual_gid = stat.file_gid(full_path)
+                if actual_uid != expected_uid or actual_gid != expected_gid
+                    if opts.verbose
+                        color.print_warning("OWNER " + rel_path + " (expected " + mtree.entry_uname(entry) + ":" + mtree.entry_gname(entry) + ")")
+                    else
+                        color.print_warning("OWNER         " + rel_path)
+                    end if
+                    mode_fail_count = mode_fail_count + 1
+                    file_ok = false
+                end if
+
+                if file_ok
+                    ok_count = ok_count + 1
+                end if
+            end if
+
+        elif str.equals(etype, "link")
+            if not stat.is_symlink(full_path)
+                if not stat.exists(full_path)
+                    color.print_warning("MISSING link: " + rel_path)
+                    missing_count = missing_count + 1
+                else
+                    color.print_warning("NOT A LINK    " + rel_path)
+                    link_fail_count = link_fail_count + 1
+                end if
+            else
+                let expected_target = mtree.entry_link(entry)
+                let actual_target = link.readlink(full_path)
+                if not str.equals(actual_target, expected_target)
+                    if opts.verbose
+                        color.print_warning("LINK " + rel_path + " -> " + actual_target + " (expected " + expected_target + ")")
+                    else
+                        color.print_warning("LINK TARGET   " + rel_path)
+                    end if
+                    link_fail_count = link_fail_count + 1
+                else
+                    ok_count = ok_count + 1
+                end if
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    // Summary
+    println("")
+    let total_issues = missing_count + checksum_fail_count + mode_fail_count + link_fail_count
+    if total_issues == 0
+        color.print_success(pkg_name + ": " + int_to_str(ok_count) + " entries verified, all OK")
+        return ec.EXIT_SUCCESS()
+    else
+        color.print_error(pkg_name + ": " + int_to_str(total_issues) + " issue(s) found")
+        if missing_count > 0
+            println("  Missing:    " + int_to_str(missing_count))
+        end if
+        if checksum_fail_count > 0
+            println("  Checksum:   " + int_to_str(checksum_fail_count))
+        end if
+        if mode_fail_count > 0
+            println("  Mode/Owner: " + int_to_str(mode_fail_count))
+        end if
+        if link_fail_count > 0
+            println("  Symlinks:   " + int_to_str(link_fail_count))
+        end if
+        println("  OK:         " + int_to_str(ok_count))
+        return ec.EXIT_PKG_CHECKSUM_MISMATCH()
+    end if
+end execute
+
+fn check_installed(name: string, root_path: string): bool
+    if str.length(root_path) > 0
+        return database.is_installed_rooted(name, root_path)
+    end if
+    return database.is_installed(name)
+end check_installed
+
+fn get_actual_root(root_path: string): string
+    if str.length(root_path) == 0
+        return "/"
+    end if
+    return root_path
+end get_actual_root
+
+proc print_usage()
+    println("Usage: coral verify <package>")
+    println("")
+    println("Verify integrity of an installed package against its manifest.")
+    println("")
+    println("Checks:")
+    println("  - Missing files, directories, and symlinks")
+    println("  - File checksum (sha256) mismatches")
+    println("  - Permission and ownership changes")
+    println("  - Broken or incorrect symlink targets")
+    println("")
+    println("Arguments:")
+    println("  <package>    Name of installed package to verify")
+    println("")
+    println("Options:")
+    println("  -v, --verbose    Show expected vs actual values for failures")
+    println("")
+    println("Examples:")
+    println("  coral verify vim")
+    println("  coral verify -v openssl")
+end print_usage
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+// Helper: convert int to octal string (for mode display)
+fn int_to_octal(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut value = n % 4096
+    mut result = ""
+    while value > 0
+        let digit = value % 8
+        result = str.concat(str.substring("01234567", digit, 1), result)
+        value = value / 8
+    end while
+
+    return result
+end int_to_octal
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/core/config.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/config.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,503 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: config.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Configuration file loading and access
+
+******************************************************************************/
+
+module core.config
+
+import types
+import io.file
+import core.str
+import encoding.toml
+
+export
+    // Configuration structure
+    type Config
+
+    // Load configuration from file (with defaults if not found)
+    fn load(): Config
+
+    // Access functions for common settings
+    fn get_arch(cfg: Config): string
+    fn get_jobs(cfg: Config): int
+    fn get_confirm(cfg: Config): bool
+    fn get_color(cfg: Config): bool
+
+    // Path accessors
+    fn get_ports_dir(cfg: Config): string
+    fn get_packages_dir(cfg: Config): string
+    fn get_sources_dir(cfg: Config): string
+    fn get_database_dir(cfg: Config): string
+    fn get_build_dir(cfg: Config): string
+    fn get_index_dir(cfg: Config): string
+    // Legacy (for compatibility)
+    fn get_work_dir(cfg: Config): string
+    fn get_pkg_dir(cfg: Config): string
+
+    // Root and prefix accessors
+    fn get_root(cfg: Config): string
+    fn get_prefix(cfg: Config): string
+    fn get_sysconfdir(cfg: Config): string
+
+    // Root-aware path accessors (prepend root to path)
+    fn get_database_dir_rooted(cfg: Config): string
+    fn get_install_prefix(cfg: Config): string
+
+    // Apply command-line overrides to config
+    fn apply_overrides(cfg: Config, root: string, prefix: string): Config
+
+    // Build settings
+    fn get_cflags(cfg: Config): string
+    fn get_cxxflags(cfg: Config): string
+    fn get_makeflags(cfg: Config): string
+    fn get_strip(cfg: Config): bool
+    fn get_fallback_to_source(cfg: Config): bool
+    fn get_compress_man(cfg: Config): bool
+    fn get_clean_work(cfg: Config): bool
+    fn get_clean_pkg(cfg: Config): bool
+    fn get_logging(cfg: Config): bool
+    fn get_quiet(cfg: Config): bool
+
+    // Network settings
+    fn get_timeout(cfg: Config): int
+    fn get_retries(cfg: Config): int
+    fn get_use_proxy(cfg: Config): bool
+
+    // Sync settings
+    fn get_auto_refresh(cfg: Config): bool
+
+    // Cache settings
+    fn get_keep_packages(cfg: Config): bool
+    fn get_keep_sources(cfg: Config): bool
+
+    // Security settings
+    fn get_require_signed_repos(cfg: Config): bool
+    fn get_require_signed_packages(cfg: Config): bool
+end export
+
+// Configuration structure
+type Config = struct
+    // [general]
+    arch: string
+    jobs: int
+    confirm: bool
+    color: bool
+
+    // [paths]
+    root: string
+    prefix: string
+    sysconfdir: string
+    ports_dir: string
+    packages_dir: string
+    sources_dir: string
+    database_dir: string
+    build_dir: string
+    index_dir: string
+    // Legacy (kept for compatibility)
+    work_dir: string
+    pkg_dir: string
+
+    // [build]
+    cflags: string
+    cxxflags: string
+    makeflags: string
+    strip: bool
+    compress_man: bool
+    fallback_to_source: bool
+    clean_work: bool
+    clean_pkg: bool
+    logging: bool
+    quiet: bool
+
+    // [network]
+    timeout: int
+    retries: int
+    use_proxy: bool
+
+    // [sync]
+    auto_refresh: bool
+
+    // [cache]
+    keep_packages: bool
+    keep_sources: bool
+
+    // [security]
+    require_signed_repos: bool
+    require_signed_packages: bool
+end Config
+
+// Create a default configuration
+fn default_config(): Config
+    return Config{
+        // General
+        arch: "amd64",
+        jobs: 0,  // 0 = auto-detect
+        confirm: true,
+        color: true,
+
+        // Paths (defaults from types)
+        root: "",
+        prefix: "/usr/local",
+        sysconfdir: "/etc",
+        ports_dir: types.get_ports_dir(),
+        packages_dir: types.get_packages_dir(),
+        sources_dir: types.get_sources_dir(),
+        database_dir: types.get_db_dir(),
+        build_dir: types.get_build_dir(),
+        index_dir: types.get_index_dir(),
+        // Legacy (for compatibility)
+        work_dir: types.get_build_dir(),
+        pkg_dir: types.get_build_dir(),
+
+        // Build
+        cflags: "-O2 -pipe",
+        cxxflags: "-O2 -pipe",
+        makeflags: "",
+        strip: true,
+        compress_man: true,
+        fallback_to_source: true,
+        clean_work: true,
+        clean_pkg: true,
+        logging: true,
+        quiet: false,
+
+        // Network
+        timeout: 30,
+        retries: 3,
+        use_proxy: false,
+
+        // Sync
+        auto_refresh: false,
+
+        // Cache
+        keep_packages: true,
+        keep_sources: true,
+
+        // Security
+        require_signed_repos: false,
+        require_signed_packages: false
+    }
+end default_config
+
+// Load configuration from file
+fn load(): Config
+    mut cfg = default_config()
+
+    // Check if config file exists
+    if not file.fileExists(types.get_config_file())
+        return cfg
+    end if
+
+    // Read config file
+    let content = file.readFile(types.get_config_file())
+    if str.length(content) == 0
+        return cfg
+    end if
+
+    // Parse TOML
+    let keys = toml.toml_alloc_keys()
+    let vals = toml.toml_alloc_values()
+    let entry_count = toml.toml_parse(content, keys, vals)
+
+    if entry_count == 0
+        return cfg
+    end if
+
+    // [general] section
+    if toml.toml_has_key(keys, vals, entry_count, "general.arch")
+        cfg.arch = toml.toml_get(keys, vals, entry_count, "general.arch")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "general.jobs")
+        cfg.jobs = toml.toml_get_int(keys, vals, entry_count, "general.jobs")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "general.confirm")
+        cfg.confirm = toml.toml_get_bool(keys, vals, entry_count, "general.confirm")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "general.color")
+        cfg.color = toml.toml_get_bool(keys, vals, entry_count, "general.color")
+    end if
+
+    // [paths] section
+    if toml.toml_has_key(keys, vals, entry_count, "paths.root")
+        cfg.root = toml.toml_get(keys, vals, entry_count, "paths.root")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "paths.prefix")
+        cfg.prefix = toml.toml_get(keys, vals, entry_count, "paths.prefix")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "paths.sysconfdir")
+        cfg.sysconfdir = toml.toml_get(keys, vals, entry_count, "paths.sysconfdir")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "paths.ports")
+        cfg.ports_dir = toml.toml_get(keys, vals, entry_count, "paths.ports")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "paths.packages")
+        cfg.packages_dir = toml.toml_get(keys, vals, entry_count, "paths.packages")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "paths.sources")
+        cfg.sources_dir = toml.toml_get(keys, vals, entry_count, "paths.sources")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "paths.database")
+        cfg.database_dir = toml.toml_get(keys, vals, entry_count, "paths.database")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "paths.build")
+        cfg.build_dir = toml.toml_get(keys, vals, entry_count, "paths.build")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "paths.index")
+        cfg.index_dir = toml.toml_get(keys, vals, entry_count, "paths.index")
+    end if
+    // Legacy path support (maps to build_dir)
+    if toml.toml_has_key(keys, vals, entry_count, "paths.work")
+        cfg.work_dir = toml.toml_get(keys, vals, entry_count, "paths.work")
+        cfg.build_dir = cfg.work_dir  // Also set new field
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "paths.pkg")
+        cfg.pkg_dir = toml.toml_get(keys, vals, entry_count, "paths.pkg")
+    end if
+
+    // [build] section
+    if toml.toml_has_key(keys, vals, entry_count, "build.cflags")
+        cfg.cflags = toml.toml_get(keys, vals, entry_count, "build.cflags")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "build.cxxflags")
+        cfg.cxxflags = toml.toml_get(keys, vals, entry_count, "build.cxxflags")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "build.makeflags")
+        cfg.makeflags = toml.toml_get(keys, vals, entry_count, "build.makeflags")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "build.strip")
+        cfg.strip = toml.toml_get_bool(keys, vals, entry_count, "build.strip")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "build.compress_man")
+        cfg.compress_man = toml.toml_get_bool(keys, vals, entry_count, "build.compress_man")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "build.fallback_to_source")
+        cfg.fallback_to_source = toml.toml_get_bool(keys, vals, entry_count, "build.fallback_to_source")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "build.clean_work")
+        cfg.clean_work = toml.toml_get_bool(keys, vals, entry_count, "build.clean_work")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "build.clean_pkg")
+        cfg.clean_pkg = toml.toml_get_bool(keys, vals, entry_count, "build.clean_pkg")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "build.logging")
+        cfg.logging = toml.toml_get_bool(keys, vals, entry_count, "build.logging")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "build.quiet")
+        cfg.quiet = toml.toml_get_bool(keys, vals, entry_count, "build.quiet")
+    end if
+
+    // [network] section
+    if toml.toml_has_key(keys, vals, entry_count, "network.timeout")
+        cfg.timeout = toml.toml_get_int(keys, vals, entry_count, "network.timeout")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "network.retries")
+        cfg.retries = toml.toml_get_int(keys, vals, entry_count, "network.retries")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "network.use_proxy")
+        cfg.use_proxy = toml.toml_get_bool(keys, vals, entry_count, "network.use_proxy")
+    end if
+
+    // [sync] section
+    if toml.toml_has_key(keys, vals, entry_count, "sync.auto_refresh")
+        cfg.auto_refresh = toml.toml_get_bool(keys, vals, entry_count, "sync.auto_refresh")
+    end if
+
+    // [cache] section
+    if toml.toml_has_key(keys, vals, entry_count, "cache.keep_packages")
+        cfg.keep_packages = toml.toml_get_bool(keys, vals, entry_count, "cache.keep_packages")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "cache.keep_sources")
+        cfg.keep_sources = toml.toml_get_bool(keys, vals, entry_count, "cache.keep_sources")
+    end if
+
+    // [security] section
+    if toml.toml_has_key(keys, vals, entry_count, "security.require_signed_repos")
+        cfg.require_signed_repos = toml.toml_get_bool(keys, vals, entry_count, "security.require_signed_repos")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "security.require_signed_packages")
+        cfg.require_signed_packages = toml.toml_get_bool(keys, vals, entry_count, "security.require_signed_packages")
+    end if
+
+    return cfg
+end load
+
+// Accessor functions
+fn get_arch(cfg: Config): string
+    return cfg.arch
+end get_arch
+
+fn get_jobs(cfg: Config): int
+    return cfg.jobs
+end get_jobs
+
+fn get_confirm(cfg: Config): bool
+    return cfg.confirm
+end get_confirm
+
+fn get_color(cfg: Config): bool
+    return cfg.color
+end get_color
+
+fn get_ports_dir(cfg: Config): string
+    return cfg.ports_dir
+end get_ports_dir
+
+fn get_packages_dir(cfg: Config): string
+    return cfg.packages_dir
+end get_packages_dir
+
+fn get_sources_dir(cfg: Config): string
+    return cfg.sources_dir
+end get_sources_dir
+
+fn get_database_dir(cfg: Config): string
+    return cfg.database_dir
+end get_database_dir
+
+fn get_build_dir(cfg: Config): string
+    return cfg.build_dir
+end get_build_dir
+
+fn get_index_dir(cfg: Config): string
+    return cfg.index_dir
+end get_index_dir
+
+// Legacy accessors (for compatibility)
+fn get_work_dir(cfg: Config): string
+    return cfg.build_dir
+end get_work_dir
+
+fn get_pkg_dir(cfg: Config): string
+    return cfg.build_dir
+end get_pkg_dir
+
+fn get_cflags(cfg: Config): string
+    return cfg.cflags
+end get_cflags
+
+fn get_cxxflags(cfg: Config): string
+    return cfg.cxxflags
+end get_cxxflags
+
+fn get_makeflags(cfg: Config): string
+    return cfg.makeflags
+end get_makeflags
+
+fn get_strip(cfg: Config): bool
+    return cfg.strip
+end get_strip
+
+fn get_fallback_to_source(cfg: Config): bool
+    return cfg.fallback_to_source
+end get_fallback_to_source
+
+fn get_compress_man(cfg: Config): bool
+    return cfg.compress_man
+end get_compress_man
+
+fn get_clean_work(cfg: Config): bool
+    return cfg.clean_work
+end get_clean_work
+
+fn get_clean_pkg(cfg: Config): bool
+    return cfg.clean_pkg
+end get_clean_pkg
+
+fn get_logging(cfg: Config): bool
+    return cfg.logging
+end get_logging
+
+fn get_quiet(cfg: Config): bool
+    return cfg.quiet
+end get_quiet
+
+fn get_timeout(cfg: Config): int
+    return cfg.timeout
+end get_timeout
+
+fn get_retries(cfg: Config): int
+    return cfg.retries
+end get_retries
+
+fn get_use_proxy(cfg: Config): bool
+    return cfg.use_proxy
+end get_use_proxy
+
+fn get_auto_refresh(cfg: Config): bool
+    return cfg.auto_refresh
+end get_auto_refresh
+
+fn get_keep_packages(cfg: Config): bool
+    return cfg.keep_packages
+end get_keep_packages
+
+fn get_keep_sources(cfg: Config): bool
+    return cfg.keep_sources
+end get_keep_sources
+
+fn get_require_signed_repos(cfg: Config): bool
+    return cfg.require_signed_repos
+end get_require_signed_repos
+
+fn get_require_signed_packages(cfg: Config): bool
+    return cfg.require_signed_packages
+end get_require_signed_packages
+
+// Get the alternate root path (empty string means /)
+fn get_root(cfg: Config): string
+    return cfg.root
+end get_root
+
+// Get the installation prefix (default /usr/local)
+fn get_prefix(cfg: Config): string
+    return cfg.prefix
+end get_prefix
+
+// Get the system configuration directory (default /etc)
+fn get_sysconfdir(cfg: Config): string
+    return cfg.sysconfdir
+end get_sysconfdir
+
+// Get database directory with root prepended
+fn get_database_dir_rooted(cfg: Config): string
+    if str.length(cfg.root) == 0
+        return cfg.database_dir
+    end if
+    return cfg.root + cfg.database_dir
+end get_database_dir_rooted
+
+// Get the full install prefix (root + prefix)
+// This is where package files should be installed
+fn get_install_prefix(cfg: Config): string
+    if str.length(cfg.root) == 0
+        return cfg.prefix
+    end if
+    return cfg.root + cfg.prefix
+end get_install_prefix
+
+// Apply command-line overrides to config
+// Call this after loading config to override with --root and --prefix flags
+fn apply_overrides(cfg: Config, root: string, prefix: string): Config
+    mut result = cfg
+    if str.length(root) > 0
+        result.root = root
+    end if
+    if str.length(prefix) > 0
+        result.prefix = prefix
+    end if
+    return result
+end apply_overrides
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/core/database.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/database.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,1061 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: database.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Installed package database management
+
+******************************************************************************/
+
+module core.database
+
+import types
+import io.file
+import io.dir
+import io.path
+import core.str
+import encoding.toml
+import util.mtree
+import sys.process
+
+export
+    // Check if a package is installed
+    fn is_installed(name: string): bool
+
+    // List all installed packages (returns count, fills names array)
+    fn list_installed(names: [string], max_count: int): int
+
+    // Get installed package details
+    fn get_installed(name: string): types.InstalledPackage
+
+    // Register a new installed package
+    fn register_package(pkg: types.InstalledPackage): bool
+
+    // Register package with file list copied from extract dir (more efficient)
+    fn register_from_extract_dir(pkg: types.InstalledPackage, extract_dir: string): bool
+
+    // Unregister (remove) a package from database
+    fn unregister_package(name: string): bool
+
+    // Get list of files owned by a package (returns count, fills files array)
+    fn get_files(name: string, files: [string], max_count: int): int
+
+    // Find which package owns a given file path
+    fn find_owner(file_path: string): string
+
+    // Find which package provides an alternative
+    fn find_provider(alternative: string): string
+
+    // Ensure database directories exist
+    fn init_db(): bool
+
+    // Scripts support
+    fn get_scripts_dir(name: string): string
+    fn store_scripts(name: string, extract_dir: string): bool
+    fn has_stored_scripts(name: string): bool
+
+    // Config files support
+    fn store_config_files(name: string, extract_dir: string): bool
+    fn get_config_files(name: string, config_files: [string], max_count: int): int
+
+    // Manifest support
+    fn get_manifest_path(name: string): string
+
+    // Root-aware versions for alternate root installation
+    fn init_db_rooted(root: string): bool
+    fn register_from_extract_dir_rooted(pkg: types.InstalledPackage, extract_dir: string, root: string): bool
+    fn store_scripts_rooted(name: string, extract_dir: string, root: string): bool
+    fn store_config_files_rooted(name: string, extract_dir: string, root: string): bool
+    fn is_installed_rooted(name: string, root: string): bool
+    fn list_installed_rooted(names: [string], max_count: int, root: string): int
+    fn get_installed_rooted(name: string, root: string): types.InstalledPackage
+    fn get_files_rooted(name: string, files: [string], max_count: int, root: string): int
+    fn find_owner_rooted(file_path: string, root: string): string
+    fn unregister_package_rooted(name: string, root: string): bool
+end export
+
+// Get the package database directory for a specific package
+fn pkg_db_dir(name: string): string
+    return path.join_path(types.get_db_dir(), name)
+end pkg_db_dir
+
+// Get the pkg.toml path for a package
+fn pkg_toml_path(name: string): string
+    return path.join_path(pkg_db_dir(name), "pkg.toml")
+end pkg_toml_path
+
+// Get the files.txt path for a package
+fn pkg_files_path(name: string): string
+    return path.join_path(pkg_db_dir(name), "files.txt")
+end pkg_files_path
+
+// Get the manifest.mtree path for a package
+fn get_manifest_path(name: string): string
+    return path.join_path(pkg_db_dir(name), "manifest.mtree")
+end get_manifest_path
+
+// Initialize the database directory structure
+fn init_db(): bool
+    if not dir.dir_exists(types.get_db_dir())
+        return dir.create_dir_all(types.get_db_dir())
+    end if
+    return true
+end init_db
+
+// Check if a package is installed by looking for its database entry
+fn is_installed(name: string): bool
+    let pkg_dir = pkg_db_dir(name)
+    if not dir.dir_exists(pkg_dir)
+        return false
+    end if
+    // Also verify pkg.toml exists
+    return file.fileExists(pkg_toml_path(name))
+end is_installed
+
+// List all installed packages by scanning the database directory
+fn list_installed(names: [string], max_count: int): int
+    if not dir.dir_exists(types.get_db_dir())
+        return 0
+    end if
+
+    mut entries: [string] = new [string](256)
+    let entry_count = dir.list_dir(types.get_db_dir(), entries, 256)
+
+    mut count = 0
+    mut i = 0
+    while i < entry_count and count < max_count
+        let entry = entries[i]
+        // Skip hidden files
+        if str.length(entry) > 0 and entry[0] != '.'
+            // Verify it's a valid package (has pkg.toml)
+            let toml_path = pkg_toml_path(entry)
+            if file.fileExists(toml_path)
+                names[count] = entry
+                count = count + 1
+            end if
+        end if
+        i = i + 1
+    end while
+
+    return count
+end list_installed
+
+// Create an empty InstalledPackage
+fn new_installed_package(): types.InstalledPackage
+    return types.new_installed_package()
+end new_installed_package
+
+// Get details of an installed package
+fn get_installed(name: string): types.InstalledPackage
+    if not is_installed(name)
+        return new_installed_package()
+    end if
+
+    // Read pkg.toml
+    let toml_path = pkg_toml_path(name)
+    let content = file.readFile(toml_path)
+    if str.length(content) == 0
+        return new_installed_package()
+    end if
+
+    // Parse TOML
+    let keys = toml.toml_alloc_keys()
+    let vals = toml.toml_alloc_values()
+    let entry_count = toml.toml_parse(content, keys, vals)
+
+    if entry_count == 0
+        return new_installed_package()
+    end if
+
+    // Build package info
+    mut pkg = new_installed_package()
+    pkg.info.name = toml.toml_get(keys, vals, entry_count, "package.name")
+    pkg.info.version = toml.toml_get(keys, vals, entry_count, "package.version")
+    pkg.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release")
+    pkg.info.description = toml.toml_get(keys, vals, entry_count, "package.description")
+    pkg.info.url = toml.toml_get(keys, vals, entry_count, "package.url")
+    pkg.info.license = toml.toml_get(keys, vals, entry_count, "package.license")
+    pkg.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer")
+    pkg.info.arch = toml.toml_get(keys, vals, entry_count, "package.arch")
+
+    // Install metadata
+    pkg.install_date = toml.toml_get(keys, vals, entry_count, "install.date")
+    pkg.install_reason = toml.toml_get(keys, vals, entry_count, "install.reason")
+
+    // Read files list
+    let files_path = pkg_files_path(name)
+    if file.fileExists(files_path)
+        let files_content = file.readFile(files_path)
+        if str.length(files_content) > 0
+            let alloc_size = count_newlines(files_content) + 16
+            mut files_arr: [string] = new [string](alloc_size)
+            let files_count = str.split(files_content, '\n', files_arr, alloc_size)
+            // Filter out empty lines
+            mut valid_count = 0
+            mut j = 0
+            while j < files_count
+                if str.length(files_arr[j]) > 0
+                    valid_count = valid_count + 1
+                end if
+                j = j + 1
+            end while
+            pkg.files = files_arr
+            pkg.files_count = valid_count
+        end if
+    end if
+
+    return pkg
+end get_installed
+
+// Generate TOML content for a package
+fn generate_pkg_toml(pkg: types.InstalledPackage): string
+    mut content = "[package]\n"
+    content = content + "name = \"" + pkg.info.name + "\"\n"
+    content = content + "version = \"" + pkg.info.version + "\"\n"
+    content = content + "release = " + int_to_str(pkg.info.release) + "\n"
+    content = content + "description = \"" + pkg.info.description + "\"\n"
+    content = content + "url = \"" + pkg.info.url + "\"\n"
+    content = content + "license = \"" + pkg.info.license + "\"\n"
+    content = content + "maintainer = \"" + pkg.info.maintainer + "\"\n"
+    content = content + "arch = \"" + pkg.info.arch + "\"\n"
+    content = content + "\n"
+    content = content + "[install]\n"
+    content = content + "date = \"" + pkg.install_date + "\"\n"
+    content = content + "reason = \"" + pkg.install_reason + "\"\n"
+    return content
+end generate_pkg_toml
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+// Register a newly installed package
+fn register_package(pkg: types.InstalledPackage): bool
+    // Ensure database directory exists
+    if not init_db()
+        return false
+    end if
+
+    // Create package directory
+    let pkg_dir = pkg_db_dir(pkg.info.name)
+    if not dir.dir_exists(pkg_dir)
+        if not dir.create_dir_all(pkg_dir)
+            return false
+        end if
+    end if
+
+    // Write pkg.toml
+    let toml_content = generate_pkg_toml(pkg)
+    let toml_path = pkg_toml_path(pkg.info.name)
+    if not file.writeFile(toml_path, toml_content)
+        return false
+    end if
+
+    // Note: files.txt is written by register_from_extract_dir for performance
+    return true
+end register_package
+
+// Count newline characters in a string to estimate line count
+fn count_newlines(s: string): int
+    let slen = str.length(s)
+    mut count = 0
+    mut i = 0
+    while i < slen
+        if s[i] == '\n'
+            count = count + 1
+        end if
+        i = i + 1
+    end while
+    return count
+end count_newlines
+
+// Process a .FOOTPRINT file content: filter and normalize paths
+// Uses str.join() for O(n) single-allocation string building
+fn process_footprint(content: string): string
+    let newline_count = count_newlines(content)
+    let alloc_size = newline_count + 16
+
+    mut lines: [string] = new [string](alloc_size)
+    let line_count = str.split(content, '\n', lines, alloc_size)
+
+    mut result_lines: [string] = new [string](alloc_size)
+    mut result_count = 0
+
+    mut i = 0
+    while i < line_count
+        mut line = str.trim_ws(lines[i])
+
+        if str.length(line) == 0
+            i = i + 1
+            continue
+        end if
+
+        // Strip leading ./
+        if str.starts_with(line, "./")
+            line = str.substring(line, 2, str.length(line) - 2)
+        end if
+
+        // Skip metadata files
+        if str.equals(line, ".PKGINFO") or str.equals(line, ".FOOTPRINT")
+            i = i + 1
+            continue
+        end if
+        if str.starts_with(line, ".SCRIPTS")
+            i = i + 1
+            continue
+        end if
+        if str.equals(line, ".CONFIG")
+            i = i + 1
+            continue
+        end if
+
+        // Skip directories (end with /)
+        let len = str.length(line)
+        if len > 0 and line[len - 1] == '/'
+            i = i + 1
+            continue
+        end if
+
+        result_lines[result_count] = line
+        result_count = result_count + 1
+        i = i + 1
+    end while
+
+    return str.join(result_lines, result_count, "\n")
+end process_footprint
+
+// Extract file paths from mtree manifest content for files.txt
+// Parses manifest entries and returns a newline-joined list of relative paths
+// (files and symlinks only, no directories, no metadata)
+fn extract_paths_from_manifest(content: string): string
+    mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192)
+    let count = mtree.parse_manifest(content, entries, 8192)
+
+    mut result_lines: [string] = new [string](8192)
+    mut result_count = 0
+
+    mut i = 0
+    while i < count
+        let etype = mtree.entry_type(entries[i])
+        let epath = mtree.entry_path(entries[i])
+
+        // Only include files and symlinks (matches legacy files.txt behavior)
+        if str.equals(etype, "file") or str.equals(etype, "link")
+            // Strip leading ./
+            if str.starts_with(epath, "./")
+                result_lines[result_count] = str.substring(epath, 2, str.length(epath) - 2)
+            else
+                result_lines[result_count] = epath
+            end if
+            result_count = result_count + 1
+        end if
+
+        i = i + 1
+    end while
+
+    return str.join(result_lines, result_count, "\n")
+end extract_paths_from_manifest
+
+// Register package and copy files list from extracted package directory
+fn register_from_extract_dir(pkg: types.InstalledPackage, extract_dir: string): bool
+    // Register the package metadata
+    if not register_package(pkg)
+        return false
+    end if
+
+    let files_path = pkg_files_path(pkg.info.name)
+
+    // Try .MANIFEST first (v2 format), fall back to .FOOTPRINT (legacy)
+    let manifest_path = path.join_path(extract_dir, ".MANIFEST")
+    if file.fileExists(manifest_path)
+        // Store the full manifest for future use (verify, manifest-based remove)
+        let manifest_store = path.join_path(pkg_db_dir(pkg.info.name), "manifest.mtree")
+        let manifest_content = file.readFile(manifest_path)
+        file.writeFile(manifest_store, manifest_content)
+
+        // Also generate files.txt for backward compatibility
+        let result = extract_paths_from_manifest(manifest_content)
+        return file.writeFile(files_path, result)
+    end if
+
+    // Legacy: .FOOTPRINT
+    let footprint_path = path.join_path(extract_dir, ".FOOTPRINT")
+    if not file.fileExists(footprint_path)
+        file.writeFile(files_path, "")
+        return true
+    end if
+
+    let content = file.readFile(footprint_path)
+    if str.length(content) == 0
+        file.writeFile(files_path, "")
+        return true
+    end if
+
+    let result = process_footprint(content)
+    return file.writeFile(files_path, result)
+end register_from_extract_dir
+
+// Helper to remove a file
+fn remove_file(file_path: string): bool
+    let cmd = "rm -f \"" + file_path + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return false
+    end if
+    let exit_code = process.process_wait(pid)
+    return exit_code == 0
+end remove_file
+
+// Unregister a package from the database
+fn unregister_package(name: string): bool
+    if not is_installed(name)
+        return true  // Already not installed
+    end if
+
+    let pkg_dir = pkg_db_dir(name)
+
+    // Remove pkg.toml
+    let toml_path = pkg_toml_path(name)
+    if file.fileExists(toml_path)
+        if not remove_file(toml_path)
+            return false
+        end if
+    end if
+
+    // Remove files.txt
+    let files_path = pkg_files_path(name)
+    if file.fileExists(files_path)
+        if not remove_file(files_path)
+            return false
+        end if
+    end if
+
+    // Remove manifest.mtree
+    let manifest_path = get_manifest_path(name)
+    if file.fileExists(manifest_path)
+        remove_file(manifest_path)
+    end if
+
+    // Remove scripts directory if exists
+    let scripts_dir = get_scripts_dir(name)
+    if dir.dir_exists(scripts_dir)
+        let cmd = "rm -rf \"" + scripts_dir + "\""
+        let pid = process.process_spawn_shell(cmd)
+        if pid > 0
+            process.process_wait(pid)
+        end if
+    end if
+
+    // Remove config.txt if exists
+    let config_path = pkg_config_path(name)
+    if file.fileExists(config_path)
+        remove_file(config_path)
+    end if
+
+    // Remove the package directory
+    return dir.remove_dir(pkg_dir)
+end unregister_package
+
+// Get list of files owned by a package
+fn get_files(name: string, files: [string], max_count: int): int
+    if not is_installed(name)
+        return 0
+    end if
+
+    let files_path = pkg_files_path(name)
+    if not file.fileExists(files_path)
+        return 0
+    end if
+
+    let content = file.readFile(files_path)
+    if str.length(content) == 0
+        return 0
+    end if
+
+    // Split by newlines — size dynamically
+    let alloc_size = count_newlines(content) + 16
+    mut all_lines: [string] = new [string](alloc_size)
+    let line_count = str.split(content, '\n', all_lines, alloc_size)
+
+    // Copy non-empty lines to output
+    mut count = 0
+    mut i = 0
+    while i < line_count and count < max_count
+        let line = str.trim_ws(all_lines[i])
+        if str.length(line) > 0
+            files[count] = line
+            count = count + 1
+        end if
+        i = i + 1
+    end while
+
+    return count
+end get_files
+
+// Find which package owns a given file
+fn find_owner(file_path: string): string
+    // Normalize the path - strip leading "/" if present
+    mut search_path = file_path
+    if str.length(search_path) > 0 and search_path[0] == '/'
+        search_path = str.substring(search_path, 1, str.length(search_path) - 1)
+    end if
+
+    // Get list of all installed packages
+    mut packages: [string] = new [string](256)
+    let pkg_count = list_installed(packages, 256)
+
+    mut i = 0
+    while i < pkg_count
+        let pkg_name = packages[i]
+
+        // Get files for this package
+        mut files: [string] = new [string](4096)
+        let file_count = get_files(pkg_name, files, 4096)
+
+        // Search for matching file
+        mut j = 0
+        while j < file_count
+            if files[j] == search_path
+                return pkg_name
+            end if
+            j = j + 1
+        end while
+
+        i = i + 1
+    end while
+
+    return ""  // No owner found
+end find_owner
+
+// Find which package provides an alternative
+fn find_provider(alternative: string): string
+    // Get list of all installed packages
+    mut packages: [string] = new [string](256)
+    let pkg_count = list_installed(packages, 256)
+
+    mut i = 0
+    while i < pkg_count
+        let pkg_name = packages[i]
+        let pkg = get_installed(pkg_name)
+
+        // Check alternatives array
+        // Note: We'd need to parse the alternatives array from pkg.toml
+        // For now, check if this package provides the alternative
+        let toml_path = pkg_toml_path(pkg_name)
+        let content = file.readFile(toml_path)
+        if str.length(content) > 0
+            let keys = toml.toml_alloc_keys()
+            let vals = toml.toml_alloc_values()
+            let entry_count = toml.toml_parse(content, keys, vals)
+
+            if toml.toml_has_key(keys, vals, entry_count, "package.alternatives")
+                let alts_val = toml.toml_get(keys, vals, entry_count, "package.alternatives")
+                // Check if the alternative is in the array
+                if str.contains(alts_val, alternative)
+                    return pkg_name
+                end if
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    return ""  // No provider found
+end find_provider
+
+// Get the scripts directory path for a package in the database
+fn get_scripts_dir(name: string): string
+    return path.join_path(pkg_db_dir(name), "scripts")
+end get_scripts_dir
+
+// Check if a package has stored scripts in the database
+fn has_stored_scripts(name: string): bool
+    let scripts_path = get_scripts_dir(name)
+    return dir.dir_exists(scripts_path)
+end has_stored_scripts
+
+// Store scripts from an extracted package to the database
+fn store_scripts(name: string, extract_dir: string): bool
+    let src_scripts = path.join_path(extract_dir, ".SCRIPTS")
+
+    // Check if source has scripts
+    if not dir.dir_exists(src_scripts)
+        return true  // No scripts is not an error
+    end if
+
+    let dest_scripts = get_scripts_dir(name)
+
+    // Create scripts directory in database
+    if not dir.dir_exists(dest_scripts)
+        if not dir.create_dir_all(dest_scripts)
+            return false
+        end if
+    end if
+
+    // Copy all script files
+    let cmd = "cp -r \"" + src_scripts + "/\"* \"" + dest_scripts + "/\" 2>/dev/null"
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return false
+    end if
+    let exit_code = process.process_wait(pid)
+    // Don't fail if cp fails (might be empty directory)
+    return true
+end store_scripts
+
+// Get the config.txt path for a package
+fn pkg_config_path(name: string): string
+    return path.join_path(pkg_db_dir(name), "config.txt")
+end pkg_config_path
+
+// Store config files list from an extracted package to the database
+fn store_config_files(name: string, extract_dir: string): bool
+    let src_config = path.join_path(extract_dir, ".CONFIG")
+
+    // Check if source has config files list
+    if not file.fileExists(src_config)
+        return true  // No config files is not an error
+    end if
+
+    let dest_config = pkg_config_path(name)
+
+    // Copy the config files list
+    let content = file.readFile(src_config)
+    if str.length(content) > 0
+        return file.writeFile(dest_config, content)
+    end if
+
+    return true
+end store_config_files
+
+// Get list of config files for a package
+fn get_config_files(name: string, config_files: [string], max_count: int): int
+    let config_path = pkg_config_path(name)
+    if not file.fileExists(config_path)
+        return 0
+    end if
+
+    let content = file.readFile(config_path)
+    if str.length(content) == 0
+        return 0
+    end if
+
+    // Split by newlines
+    mut lines: [string] = new [string](max_count)
+    let line_count = str.split(content, '\n', lines, max_count)
+
+    // Filter out empty lines and comments
+    mut count = 0
+    mut i = 0
+    while i < line_count and count < max_count
+        let line = str.trim_ws(lines[i])
+        if str.length(line) > 0 and line[0] != '#'
+            config_files[count] = line
+            count = count + 1
+        end if
+        i = i + 1
+    end while
+
+    return count
+end get_config_files
+
+// ============================================================================
+// Root-aware functions for alternate root installation
+// ============================================================================
+
+// Get the rooted database directory
+fn get_db_dir_rooted(root: string): string
+    if str.length(root) == 0
+        return types.get_db_dir()
+    end if
+    return root + types.get_db_dir()
+end get_db_dir_rooted
+
+// Get the rooted package database directory
+fn pkg_db_dir_rooted(name: string, root: string): string
+    return path.join_path(get_db_dir_rooted(root), name)
+end pkg_db_dir_rooted
+
+// Get the rooted pkg.toml path
+fn pkg_toml_path_rooted(name: string, root: string): string
+    return path.join_path(pkg_db_dir_rooted(name, root), "pkg.toml")
+end pkg_toml_path_rooted
+
+// Get the rooted files.txt path
+fn pkg_files_path_rooted(name: string, root: string): string
+    return path.join_path(pkg_db_dir_rooted(name, root), "files.txt")
+end pkg_files_path_rooted
+
+// Initialize database with alternate root
+fn init_db_rooted(root: string): bool
+    let db_dir = get_db_dir_rooted(root)
+    if not dir.dir_exists(db_dir)
+        return dir.create_dir_all(db_dir)
+    end if
+    return true
+end init_db_rooted
+
+// Check if package is installed in alternate root
+fn is_installed_rooted(name: string, root: string): bool
+    let pkg_dir = pkg_db_dir_rooted(name, root)
+    if not dir.dir_exists(pkg_dir)
+        return false
+    end if
+    return file.fileExists(pkg_toml_path_rooted(name, root))
+end is_installed_rooted
+
+// Register package with alternate root
+fn register_package_rooted(pkg: types.InstalledPackage, root: string): bool
+    // Ensure database directory exists
+    if not init_db_rooted(root)
+        return false
+    end if
+
+    // Create package directory
+    let pkg_dir = pkg_db_dir_rooted(pkg.info.name, root)
+    if not dir.dir_exists(pkg_dir)
+        if not dir.create_dir_all(pkg_dir)
+            return false
+        end if
+    end if
+
+    // Write pkg.toml
+    let toml_content = generate_pkg_toml(pkg)
+    let toml_path = pkg_toml_path_rooted(pkg.info.name, root)
+    if not file.writeFile(toml_path, toml_content)
+        return false
+    end if
+
+    return true
+end register_package_rooted
+
+// Register package from extract dir with alternate root
+fn register_from_extract_dir_rooted(pkg: types.InstalledPackage, extract_dir: string, root: string): bool
+    // Register the package metadata
+    if not register_package_rooted(pkg, root)
+        return false
+    end if
+
+    let files_path = pkg_files_path_rooted(pkg.info.name, root)
+
+    // Try .MANIFEST first (v2 format), fall back to .FOOTPRINT (legacy)
+    let manifest_path = path.join_path(extract_dir, ".MANIFEST")
+    if file.fileExists(manifest_path)
+        // Store the full manifest
+        let manifest_store = path.join_path(pkg_db_dir_rooted(pkg.info.name, root), "manifest.mtree")
+        let manifest_content = file.readFile(manifest_path)
+        file.writeFile(manifest_store, manifest_content)
+
+        // Also generate files.txt for backward compatibility
+        let result = extract_paths_from_manifest(manifest_content)
+        return file.writeFile(files_path, result)
+    end if
+
+    // Legacy: .FOOTPRINT
+    let footprint_path = path.join_path(extract_dir, ".FOOTPRINT")
+    if not file.fileExists(footprint_path)
+        file.writeFile(files_path, "")
+        return true
+    end if
+
+    let content = file.readFile(footprint_path)
+    if str.length(content) == 0
+        file.writeFile(files_path, "")
+        return true
+    end if
+
+    let result = process_footprint(content)
+    return file.writeFile(files_path, result)
+end register_from_extract_dir_rooted
+
+// Get scripts directory with alternate root
+fn get_scripts_dir_rooted(name: string, root: string): string
+    return path.join_path(pkg_db_dir_rooted(name, root), "scripts")
+end get_scripts_dir_rooted
+
+// Store scripts with alternate root
+fn store_scripts_rooted(name: string, extract_dir: string, root: string): bool
+    let src_scripts = path.join_path(extract_dir, ".SCRIPTS")
+
+    if not dir.dir_exists(src_scripts)
+        return true
+    end if
+
+    let dest_scripts = get_scripts_dir_rooted(name, root)
+
+    if not dir.dir_exists(dest_scripts)
+        if not dir.create_dir_all(dest_scripts)
+            return false
+        end if
+    end if
+
+    let cmd = "cp -r \"" + src_scripts + "/\"* \"" + dest_scripts + "/\" 2>/dev/null"
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return false
+    end if
+    process.process_wait(pid)
+    return true
+end store_scripts_rooted
+
+// Get config path with alternate root
+fn pkg_config_path_rooted(name: string, root: string): string
+    return path.join_path(pkg_db_dir_rooted(name, root), "config.txt")
+end pkg_config_path_rooted
+
+// Store config files with alternate root
+fn store_config_files_rooted(name: string, extract_dir: string, root: string): bool
+    let src_config = path.join_path(extract_dir, ".CONFIG")
+
+    if not file.fileExists(src_config)
+        return true
+    end if
+
+    let dest_config = pkg_config_path_rooted(name, root)
+
+    let content = file.readFile(src_config)
+    if str.length(content) > 0
+        return file.writeFile(dest_config, content)
+    end if
+
+    return true
+end store_config_files_rooted
+
+// List all installed packages in alternate root
+fn list_installed_rooted(names: [string], max_count: int, root: string): int
+    let db_dir = get_db_dir_rooted(root)
+    if not dir.dir_exists(db_dir)
+        return 0
+    end if
+
+    mut entries: [string] = new [string](256)
+    let entry_count = dir.list_dir(db_dir, entries, 256)
+
+    mut count = 0
+    mut i = 0
+    while i < entry_count and count < max_count
+        let entry = entries[i]
+        // Skip hidden files
+        if str.length(entry) > 0 and entry[0] != '.'
+            // Verify it's a valid package (has pkg.toml)
+            let toml_path = pkg_toml_path_rooted(entry, root)
+            if file.fileExists(toml_path)
+                names[count] = entry
+                count = count + 1
+            end if
+        end if
+        i = i + 1
+    end while
+
+    return count
+end list_installed_rooted
+
+// Get details of an installed package in alternate root
+fn get_installed_rooted(name: string, root: string): types.InstalledPackage
+    if not is_installed_rooted(name, root)
+        return new_installed_package()
+    end if
+
+    // Read pkg.toml
+    let toml_path = pkg_toml_path_rooted(name, root)
+    let content = file.readFile(toml_path)
+    if str.length(content) == 0
+        return new_installed_package()
+    end if
+
+    // Parse TOML
+    let keys = toml.toml_alloc_keys()
+    let vals = toml.toml_alloc_values()
+    let entry_count = toml.toml_parse(content, keys, vals)
+
+    if entry_count == 0
+        return new_installed_package()
+    end if
+
+    // Build package info
+    mut pkg = new_installed_package()
+    pkg.info.name = toml.toml_get(keys, vals, entry_count, "package.name")
+    pkg.info.version = toml.toml_get(keys, vals, entry_count, "package.version")
+    pkg.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release")
+    pkg.info.description = toml.toml_get(keys, vals, entry_count, "package.description")
+    pkg.info.url = toml.toml_get(keys, vals, entry_count, "package.url")
+    pkg.info.license = toml.toml_get(keys, vals, entry_count, "package.license")
+    pkg.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer")
+    pkg.info.arch = toml.toml_get(keys, vals, entry_count, "package.arch")
+
+    // Install metadata
+    pkg.install_date = toml.toml_get(keys, vals, entry_count, "install.date")
+    pkg.install_reason = toml.toml_get(keys, vals, entry_count, "install.reason")
+
+    // Read files list
+    let files_path = pkg_files_path_rooted(name, root)
+    if file.fileExists(files_path)
+        let files_content = file.readFile(files_path)
+        if str.length(files_content) > 0
+            let alloc_size = count_newlines(files_content) + 16
+            mut files_arr: [string] = new [string](alloc_size)
+            let files_count = str.split(files_content, '\n', files_arr, alloc_size)
+            // Filter out empty lines
+            mut valid_count = 0
+            mut j = 0
+            while j < files_count
+                if str.length(files_arr[j]) > 0
+                    valid_count = valid_count + 1
+                end if
+                j = j + 1
+            end while
+            pkg.files = files_arr
+            pkg.files_count = valid_count
+        end if
+    end if
+
+    return pkg
+end get_installed_rooted
+
+// Get list of files owned by a package in alternate root
+fn get_files_rooted(name: string, files: [string], max_count: int, root: string): int
+    if not is_installed_rooted(name, root)
+        return 0
+    end if
+
+    let files_path = pkg_files_path_rooted(name, root)
+    if not file.fileExists(files_path)
+        return 0
+    end if
+
+    let content = file.readFile(files_path)
+    if str.length(content) == 0
+        return 0
+    end if
+
+    // Split by newlines — size dynamically
+    let alloc_size = count_newlines(content) + 16
+    mut all_lines: [string] = new [string](alloc_size)
+    let line_count = str.split(content, '\n', all_lines, alloc_size)
+
+    // Copy non-empty lines to output
+    mut count = 0
+    mut i = 0
+    while i < line_count and count < max_count
+        let line = str.trim_ws(all_lines[i])
+        if str.length(line) > 0
+            files[count] = line
+            count = count + 1
+        end if
+        i = i + 1
+    end while
+
+    return count
+end get_files_rooted
+
+// Find which package owns a given file in alternate root
+fn find_owner_rooted(file_path: string, root: string): string
+    // Normalize the path - strip leading "/" if present
+    mut search_path = file_path
+    if str.length(search_path) > 0 and search_path[0] == '/'
+        search_path = str.substring(search_path, 1, str.length(search_path) - 1)
+    end if
+
+    // Get list of all installed packages in this root
+    mut packages: [string] = new [string](256)
+    let pkg_count = list_installed_rooted(packages, 256, root)
+
+    mut i = 0
+    while i < pkg_count
+        let pkg_name = packages[i]
+
+        // Get files for this package
+        mut files: [string] = new [string](4096)
+        let file_count = get_files_rooted(pkg_name, files, 4096, root)
+
+        // Search for matching file
+        mut j = 0
+        while j < file_count
+            if files[j] == search_path
+                return pkg_name
+            end if
+            j = j + 1
+        end while
+
+        i = i + 1
+    end while
+
+    return ""  // No owner found
+end find_owner_rooted
+
+// Unregister a package from the database in alternate root
+fn unregister_package_rooted(name: string, root: string): bool
+    if not is_installed_rooted(name, root)
+        return true  // Already not installed
+    end if
+
+    let pkg_dir = pkg_db_dir_rooted(name, root)
+
+    // Remove pkg.toml
+    let toml_path = pkg_toml_path_rooted(name, root)
+    if file.fileExists(toml_path)
+        if not remove_file(toml_path)
+            return false
+        end if
+    end if
+
+    // Remove files.txt
+    let files_path = pkg_files_path_rooted(name, root)
+    if file.fileExists(files_path)
+        if not remove_file(files_path)
+            return false
+        end if
+    end if
+
+    // Remove scripts directory if exists
+    let scripts_dir = get_scripts_dir_rooted(name, root)
+    if dir.dir_exists(scripts_dir)
+        let cmd = "rm -rf \"" + scripts_dir + "\""
+        let pid = process.process_spawn_shell(cmd)
+        if pid > 0
+            process.process_wait(pid)
+        end if
+    end if
+
+    // Remove config.txt if exists
+    let config_path = pkg_config_path_rooted(name, root)
+    if file.fileExists(config_path)
+        remove_file(config_path)
+    end if
+
+    // Remove the package directory
+    return dir.remove_dir(pkg_dir)
+end unregister_package_rooted
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/core/groups.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/groups.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,211 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: groups.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Package groups support for Coral (@groupname)
+
+******************************************************************************/
+
+module core.groups
+
+import core.str
+import io.file
+import io.dir
+import io.path
+import encoding.toml
+import types
+
+export
+    type GroupResult
+
+    fn load_group(name: string): GroupResult
+    fn list_groups(groups: [types.PackageGroup], max_count: int): int
+    fn expand_group(name: string, packages: [string], max_count: int): int
+    fn is_group(name: string): bool
+end export
+
+// Result for group loading
+type GroupResult = struct
+    success: bool
+    group: types.PackageGroup
+    error: string
+end GroupResult
+
+// Get the groups directory
+fn get_groups_dir(): string
+    return types.get_groups_dir()
+end get_groups_dir
+
+// Create empty group
+fn new_group(): types.PackageGroup
+    return types.new_package_group()
+end new_group
+
+// Create success result
+fn success_result(g: types.PackageGroup): GroupResult
+    return GroupResult{ success: true, group: g, error: "" }
+end success_result
+
+// Create error result
+fn error_result(msg: string): GroupResult
+    return GroupResult{ success: false, group: new_group(), error: msg }
+end error_result
+
+// Check if a package name is a group reference (starts with @)
+fn is_group(name: string): bool
+    if str.length(name) < 2
+        return false
+    end if
+    return name[0] == '@'
+end is_group
+
+// Load a group definition from TOML file
+fn load_group(name: string): GroupResult
+    // Remove @ prefix if present
+    mut group_name = name
+    if name[0] == '@'
+        group_name = str.substring(name, 1, str.length(name) - 1)
+    end if
+
+    let groups_dir = get_groups_dir()
+    let group_file = path.join_path(groups_dir, group_name + ".toml")
+
+    if not file.fileExists(group_file)
+        return error_result("Group not found: " + group_name)
+    end if
+
+    let content = file.readFile(group_file)
+    if str.length(content) == 0
+        return error_result("Failed to read group file")
+    end if
+
+    // Parse TOML using key/value arrays
+    let keys = toml.toml_alloc_keys()
+    let vals = toml.toml_alloc_values()
+    let entry_count = toml.toml_parse(content, keys, vals)
+
+    if entry_count == 0
+        return error_result("Invalid group file format")
+    end if
+
+    mut group = new_group()
+    group.name = group_name
+
+    // Get description
+    if toml.toml_has_key(keys, vals, entry_count, "group.description")
+        group.description = toml.toml_get(keys, vals, entry_count, "group.description")
+    end if
+
+    // Get members list (packages array)
+    if toml.toml_has_key(keys, vals, entry_count, "group.packages")
+        let members_str = toml.toml_get(keys, vals, entry_count, "group.packages")
+        group.members_count = parse_toml_array(members_str, group.members, 256)
+    end if
+
+    return success_result(group)
+end load_group
+
+// List all available groups
+fn list_groups(groups: [types.PackageGroup], max_count: int): int
+    let groups_dir = get_groups_dir()
+
+    if not dir.dir_exists(groups_dir)
+        return 0
+    end if
+
+    mut entries: [string] = new [string](64)
+    let entry_count = dir.list_dir(groups_dir, entries, 64)
+
+    mut count = 0
+    mut i = 0
+    while i < entry_count and count < max_count
+        let entry = entries[i]
+        // Look for .toml files
+        if str.ends_with(entry, ".toml")
+            let name_len = str.length(entry) - 5
+            let name = str.substring(entry, 0, name_len)
+
+            let result = load_group(name)
+            if result.success
+                groups[count] = result.group
+                count = count + 1
+            end if
+        end if
+        i = i + 1
+    end while
+
+    return count
+end list_groups
+
+// Expand a group to its member packages
+fn expand_group(name: string, packages: [string], max_count: int): int
+    let result = load_group(name)
+
+    if not result.success
+        return 0
+    end if
+
+    let group = result.group
+    mut count = 0
+
+    while count < group.members_count and count < max_count
+        packages[count] = group.members[count]
+        count = count + 1
+    end while
+
+    return count
+end expand_group
+
+// Parse TOML array value into string array
+// Format: ["item1", "item2", "item3"]
+fn parse_toml_array(value: string, result: [string], max_items: int): int
+    let len = str.length(value)
+    if len < 2
+        return 0
+    end if
+
+    // Check for array brackets
+    if value[0] != '['
+        return 0
+    end if
+
+    mut count = 0
+    mut i = 1
+    mut in_string = false
+    mut item_start = 0
+
+    while i < len and count < max_items
+        let c = value[i]
+
+        if c == '"' and not in_string
+            in_string = true
+            item_start = i + 1
+        elif c == '"' and in_string
+            // End of string item
+            let item_len = i - item_start
+            if item_len > 0
+                result[count] = str.substring(value, item_start, item_len)
+                count = count + 1
+            end if
+            in_string = false
+        elif c == ']' and not in_string
+            // End of array
+            return count
+        end if
+
+        i = i + 1
+    end while
+
+    return count
+end parse_toml_array
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/core/package.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/package.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,957 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: package.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Package archive handling - extract, read metadata, install files
+
+******************************************************************************/
+
+module core.package
+
+import types
+import io.file
+import io.dir
+import io.path
+import core.str
+import encoding.toml
+import util.archive
+import util.mtree
+import util.checksum
+import sys.process
+import fs.stat
+import fs.perm
+import fs.link
+import fs.ops
+
+export
+    // Package extraction result
+    type ExtractResult
+
+    // Extract a package to a temporary directory
+    fn extract_package(pkg_path: string, dest_dir: string): ExtractResult
+
+    // Read package info from an extracted package or archive
+    fn read_pkginfo(pkg_path: string): types.PackageInfo
+
+    // Read file list from an extracted package (legacy .FOOTPRINT or .MANIFEST)
+    fn read_footprint(pkg_path: string, files: [string], max_count: int): int
+
+    // Read manifest entries from an extracted package
+    fn read_manifest(pkg_dir: string, entries: [mtree.ManifestEntry], max_count: int): int
+
+    // Install files from extracted package to root (manifest-driven)
+    fn install_files(src_dir: string, root: string): bool
+
+    // Remove files listed in footprint from root
+    fn remove_files(files: [string], file_count: int, root: string): bool
+
+    // Remove files using manifest entries (with explicit directory tracking)
+    fn remove_files_manifest(entries: [mtree.ManifestEntry], entry_count: int, root: string): bool
+
+    // Verify package integrity (check all files exist in extracted dir)
+    fn verify_package(pkg_dir: string): bool
+
+    // Install files with old manifest for upgrade config protection
+    fn install_files_upgrade(src_dir: string, root: string, old_entries: [mtree.ManifestEntry], old_entry_count: int): bool
+
+    // Install scripts support (version passed as $VERSION to scripts)
+    fn has_scripts(pkg_dir: string): bool
+    fn run_pre_install(pkg_dir: string, version: string): bool
+    fn run_post_install(pkg_dir: string, version: string): bool
+    fn run_pre_remove(pkg_dir: string, version: string): bool
+    fn run_post_remove(pkg_dir: string, version: string): bool
+
+    // Upgrade scripts ($OLD_VERSION $NEW_VERSION passed to scripts)
+    fn run_pre_upgrade(pkg_dir: string, old_version: string, new_version: string): bool
+    fn run_post_upgrade(pkg_dir: string, old_version: string, new_version: string): bool
+
+    // Run scripts from a stored scripts directory (for remove/upgrade operations)
+    fn run_stored_pre_remove(scripts_dir: string, version: string): bool
+    fn run_stored_post_remove(scripts_dir: string, version: string): bool
+    fn run_stored_pre_upgrade(scripts_dir: string, old_version: string, new_version: string): bool
+    fn run_stored_post_upgrade(scripts_dir: string, old_version: string, new_version: string): bool
+
+    // Config file protection
+    fn read_config_files(pkg_dir: string, config_files: [string], max_count: int): int
+    fn is_config_file(rel_path: string, config_files: [string], config_count: int): bool
+    fn install_config_file(src_path: string, dest_path: string, manifest_sha256: string, stored_sha256: string): bool
+end export
+
+// Extraction result
+type ExtractResult = struct
+    success: bool
+    extract_dir: string
+    error: string
+end ExtractResult
+
+// Create a failed extraction result
+fn extract_error(msg: string): ExtractResult
+    return ExtractResult{ success: false, extract_dir: "", error: msg }
+end extract_error
+
+// Create a successful extraction result
+fn extract_success(dir: string): ExtractResult
+    return ExtractResult{ success: true, extract_dir: dir, error: "" }
+end extract_success
+
+// Extract a package archive to destination directory
+fn extract_package(pkg_path: string, dest_dir: string): ExtractResult
+    // Verify package exists
+    if not file.fileExists(pkg_path)
+        return extract_error("Package file not found: " + pkg_path)
+    end if
+
+    // Ensure destination directory exists
+    if not dir.dir_exists(dest_dir)
+        if not dir.create_dir_all(dest_dir)
+            return extract_error("Failed to create extraction directory: " + dest_dir)
+        end if
+    end if
+
+    // Extract using archive module
+    if not archive.extract(pkg_path, dest_dir)
+        return extract_error("Failed to extract package: " + pkg_path)
+    end if
+
+    // Verify .PKGINFO exists
+    let pkginfo_path = path.join_path(dest_dir, ".PKGINFO")
+    if not file.fileExists(pkginfo_path)
+        return extract_error("Invalid package: missing .PKGINFO")
+    end if
+
+    return extract_success(dest_dir)
+end extract_package
+
+// Read package info from .PKGINFO in an extracted package directory
+fn read_pkginfo(pkg_dir: string): types.PackageInfo
+    mut info = types.new_package_info()
+
+    let pkginfo_path = path.join_path(pkg_dir, ".PKGINFO")
+    if not file.fileExists(pkginfo_path)
+        return info
+    end if
+
+    let content = file.readFile(pkginfo_path)
+    if str.length(content) == 0
+        return info
+    end if
+
+    // Parse TOML
+    let keys = toml.toml_alloc_keys()
+    let vals = toml.toml_alloc_values()
+    let entry_count = toml.toml_parse(content, keys, vals)
+
+    if entry_count == 0
+        return info
+    end if
+
+    // Extract fields
+    info.name = toml.toml_get(keys, vals, entry_count, "package.name")
+    info.version = toml.toml_get(keys, vals, entry_count, "package.version")
+    info.release = toml.toml_get_int(keys, vals, entry_count, "package.release")
+    info.description = toml.toml_get(keys, vals, entry_count, "package.description")
+    info.url = toml.toml_get(keys, vals, entry_count, "package.url")
+    info.license = toml.toml_get(keys, vals, entry_count, "package.license")
+    info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer")
+    info.arch = toml.toml_get(keys, vals, entry_count, "package.arch")
+
+    // Parse [dependencies] section (optional — legacy packages won't have it)
+    if toml.toml_has_key(keys, vals, entry_count, "dependencies.runtime")
+        let rt_val = toml.toml_get(keys, vals, entry_count, "dependencies.runtime")
+        info.runtime_deps_count = parse_toml_array(rt_val, info.runtime_deps, 64)
+    end if
+
+    if toml.toml_has_key(keys, vals, entry_count, "dependencies.build")
+        let bd_val = toml.toml_get(keys, vals, entry_count, "dependencies.build")
+        info.build_deps_count = parse_toml_array(bd_val, info.build_deps, 64)
+    end if
+
+    return info
+end read_pkginfo
+
+// Read file list from .MANIFEST (v2) or .FOOTPRINT (legacy)
+// Returns a flat list of relative file paths (files and symlinks only)
+fn read_footprint(pkg_dir: string, files: [string], max_count: int): int
+    // Try .MANIFEST first (v2 format)
+    let manifest_path = path.join_path(pkg_dir, ".MANIFEST")
+    if file.fileExists(manifest_path)
+        let content = file.readFile(manifest_path)
+        if str.length(content) > 0
+            mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](max_count)
+            let entry_count = mtree.parse_manifest(content, entries, max_count)
+
+            mut count = 0
+            mut i = 0
+            while i < entry_count and count < max_count
+                let etype = mtree.entry_type(entries[i])
+                // Only include files and symlinks (matches legacy behavior)
+                if str.equals(etype, "file") or str.equals(etype, "link")
+                    let epath = mtree.entry_path(entries[i])
+                    if str.starts_with(epath, "./")
+                        files[count] = str.substring(epath, 2, str.length(epath) - 2)
+                    else
+                        files[count] = epath
+                    end if
+                    count = count + 1
+                end if
+                i = i + 1
+            end while
+
+            return count
+        end if
+    end if
+
+    // Fallback: legacy .FOOTPRINT
+    let footprint_path = path.join_path(pkg_dir, ".FOOTPRINT")
+    if not file.fileExists(footprint_path)
+        return 0
+    end if
+
+    let content = file.readFile(footprint_path)
+    if str.length(content) == 0
+        return 0
+    end if
+
+    mut lines: [string] = new [string](max_count)
+    let line_count = str.split(content, '\n', lines, max_count)
+
+    mut count = 0
+    mut i = 0
+    while i < line_count and count < max_count
+        let line = str.trim_ws(lines[i])
+
+        if str.length(line) == 0
+            i = i + 1
+            continue
+        end if
+
+        if str.equals(line, "./.PKGINFO") or str.equals(line, "./.FOOTPRINT")
+            i = i + 1
+            continue
+        end if
+        if str.equals(line, ".PKGINFO") or str.equals(line, ".FOOTPRINT")
+            i = i + 1
+            continue
+        end if
+
+        let len = str.length(line)
+        if len > 0 and line[len - 1] == '/'
+            i = i + 1
+            continue
+        end if
+
+        if str.starts_with(line, "./")
+            files[count] = str.substring(line, 2, len - 2)
+        else
+            files[count] = line
+        end if
+        count = count + 1
+
+        i = i + 1
+    end while
+
+    return count
+end read_footprint
+
+// Read manifest entries from .MANIFEST in an extracted package directory
+fn read_manifest(pkg_dir: string, entries: [mtree.ManifestEntry], max_count: int): int
+    let manifest_path = path.join_path(pkg_dir, ".MANIFEST")
+    if not file.fileExists(manifest_path)
+        return 0
+    end if
+
+    let content = file.readFile(manifest_path)
+    if str.length(content) == 0
+        return 0
+    end if
+
+    return mtree.parse_manifest(content, entries, max_count)
+end read_manifest
+
+// Install files from extracted package to root directory using manifest
+fn install_files(src_dir: string, root: string): bool
+    // Read config file list for protection checks
+    mut config_files: [string] = new [string](256)
+    let config_count = read_config_files(src_dir, config_files, 256)
+
+    // Parse .MANIFEST
+    mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192)
+    let entry_count = read_manifest(src_dir, entries, 8192)
+
+    if entry_count == 0
+        return false
+    end if
+
+    // Walk manifest entries in order (dirs before their contents)
+    mut i = 0
+    while i < entry_count
+        let entry = entries[i]
+        let etype = mtree.entry_type(entry)
+        let epath = mtree.entry_path(entry)
+
+        // Strip leading ./ from manifest path to get relative path
+        mut rel_path = epath
+        if str.starts_with(epath, "./")
+            rel_path = str.substring(epath, 2, str.length(epath) - 2)
+        end if
+
+        let dest_path = path.join_path(root, rel_path)
+        let src_path = path.join_path(src_dir, rel_path)
+        let mode = mtree.entry_mode(entry)
+        let uid = mtree.name_to_uid(mtree.entry_uname(entry))
+        let gid = mtree.name_to_gid(mtree.entry_gname(entry))
+
+        if str.equals(etype, "dir")
+            // Create directory if it doesn't exist
+            if not dir.dir_exists(dest_path)
+                if not dir.create_dir_all(dest_path)
+                    return false
+                end if
+            end if
+            perm.chmod(dest_path, mode)
+            perm.chown(dest_path, uid, gid)
+
+        elif str.equals(etype, "file")
+            // Check if this is a config-protected file
+            if is_config_file(rel_path, config_files, config_count)
+                // Config file — use checksum-based protection
+                let manifest_sha = mtree.entry_sha256(entry)
+                install_config_file(src_path, dest_path, manifest_sha, "")
+            else
+                // Ensure parent directory exists
+                let parent = path.dirname(dest_path)
+                if str.length(parent) > 0 and not dir.dir_exists(parent)
+                    dir.create_dir_all(parent)
+                end if
+
+                // Remove any conflicting target (symlink, file, or dir)
+                if stat.is_symlink(dest_path)
+                    ops.remove_file(dest_path)
+                elif stat.exists(dest_path)
+                    ops.remove_file(dest_path)
+                end if
+
+                // Copy file preserving permissions from source
+                if not ops.copy_file_preserve(src_path, dest_path)
+                    return false
+                end if
+
+                // Set permissions and ownership from manifest
+                perm.chmod(dest_path, mode)
+                perm.chown(dest_path, uid, gid)
+
+                // Verify sha256 after copy
+                let manifest_sha = mtree.entry_sha256(entry)
+                if str.length(manifest_sha) > 0
+                    let installed_sha = checksum.sha256_file(dest_path)
+                    if not str.equals(installed_sha, manifest_sha)
+                        return false
+                    end if
+                end if
+            end if
+
+        elif str.equals(etype, "link")
+            let link_target = mtree.entry_link(entry)
+
+            // Remove any conflicting target
+            if stat.is_symlink(dest_path) or stat.exists(dest_path)
+                ops.remove_file(dest_path)
+            end if
+
+            // Ensure parent directory exists
+            let parent = path.dirname(dest_path)
+            if str.length(parent) > 0 and not dir.dir_exists(parent)
+                dir.create_dir_all(parent)
+            end if
+
+            // Create symlink
+            if not link.symlink(link_target, dest_path)
+                return false
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    return true
+end install_files
+
+// Remove files from root directory based on flat file list (legacy)
+fn remove_files(files: [string], file_count: int, root: string): bool
+    // Remove files in reverse order
+    mut i = file_count - 1
+    while i >= 0
+        let rel_path = files[i]
+        let full_path = path.join_path(root, rel_path)
+
+        if stat.is_symlink(full_path) or stat.exists(full_path)
+            ops.remove_file(full_path)
+        end if
+
+        i = i - 1
+    end while
+
+    // Try to remove empty parent directories
+    i = file_count - 1
+    while i >= 0
+        let rel_path = files[i]
+        let full_path = path.join_path(root, rel_path)
+        let parent = path.dirname(full_path)
+
+        if str.length(parent) > 0 and dir.dir_exists(parent)
+            dir.remove_dir(parent)
+        end if
+
+        i = i - 1
+    end while
+
+    return true
+end remove_files
+
+// Remove files using manifest entries (with explicit directory tracking)
+// Processes in reverse order: files/symlinks first, then directories
+fn remove_files_manifest(entries: [mtree.ManifestEntry], entry_count: int, root: string): bool
+    mut all_success = true
+
+    // First pass: remove files and symlinks in reverse order
+    mut i = entry_count - 1
+    while i >= 0
+        let entry = entries[i]
+        let etype = mtree.entry_type(entry)
+        let epath = mtree.entry_path(entry)
+
+        mut rel_path = epath
+        if str.starts_with(epath, "./")
+            rel_path = str.substring(epath, 2, str.length(epath) - 2)
+        end if
+
+        let full_path = path.join_path(root, rel_path)
+
+        if str.equals(etype, "file") or str.equals(etype, "link")
+            if stat.is_symlink(full_path) or stat.exists(full_path)
+                if not ops.remove_file(full_path)
+                    all_success = false
+                end if
+            end if
+        end if
+
+        i = i - 1
+    end while
+
+    // Second pass: remove directories in reverse order (deepest first)
+    i = entry_count - 1
+    while i >= 0
+        let entry = entries[i]
+        let etype = mtree.entry_type(entry)
+        let epath = mtree.entry_path(entry)
+
+        if str.equals(etype, "dir")
+            mut rel_path = epath
+            if str.starts_with(epath, "./")
+                rel_path = str.substring(epath, 2, str.length(epath) - 2)
+            end if
+
+            let full_path = path.join_path(root, rel_path)
+
+            // Only remove empty directories (remove_dir fails on non-empty)
+            if dir.dir_exists(full_path)
+                dir.remove_dir(full_path)
+            end if
+        end if
+
+        i = i - 1
+    end while
+
+    return all_success
+end remove_files_manifest
+
+// Verify package has required files
+fn verify_package(pkg_dir: string): bool
+    // Check for .PKGINFO
+    let pkginfo_path = path.join_path(pkg_dir, ".PKGINFO")
+    if not file.fileExists(pkginfo_path)
+        return false
+    end if
+
+    // Check for .MANIFEST (or legacy .FOOTPRINT)
+    let manifest_path = path.join_path(pkg_dir, ".MANIFEST")
+    let footprint_path = path.join_path(pkg_dir, ".FOOTPRINT")
+    if not file.fileExists(manifest_path) and not file.fileExists(footprint_path)
+        return false
+    end if
+
+    // Verify we can read package info
+    let info = read_pkginfo(pkg_dir)
+    if str.length(info.name) == 0
+        return false
+    end if
+
+    return true
+end verify_package
+
+// Check if package has install/remove scripts
+fn has_scripts(pkg_dir: string): bool
+    let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS")
+    return dir.dir_exists(scripts_dir)
+end has_scripts
+
+// Run a script from the .SCRIPTS directory
+fn run_script(pkg_dir: string, script_name: string, script_args: string): bool
+    let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS")
+    let script_path = path.join_path(scripts_dir, script_name)
+
+    if not file.fileExists(script_path)
+        return true
+    end if
+
+    mut cmd = ""
+
+    // Shell scripts (.sh) - run with zsh
+    if str.ends_with(script_name, ".sh")
+        cmd = "zsh \"" + script_path + "\""
+    else
+        // Default: try to execute directly (binary or has shebang)
+        cmd = "\"" + script_path + "\""
+    end if
+
+    if str.length(script_args) > 0
+        cmd = cmd + " " + script_args
+    end if
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return false
+    end if
+    let exit_code = process.process_wait(pid)
+    return exit_code == 0
+end run_script
+
+// Run pre-install script (passes $VERSION)
+fn run_pre_install(pkg_dir: string, version: string): bool
+    if not has_scripts(pkg_dir)
+        return true
+    end if
+    let args = "\"" + version + "\""
+    let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS")
+    // Try .sh first, then bare executable
+    let sh_path = path.join_path(scripts_dir, "pre-install.sh")
+    if file.fileExists(sh_path)
+        return run_script(pkg_dir, "pre-install.sh", args)
+    end if
+    return run_script(pkg_dir, "pre-install", args)
+end run_pre_install
+
+// Run post-install script (passes $VERSION)
+fn run_post_install(pkg_dir: string, version: string): bool
+    if not has_scripts(pkg_dir)
+        return true
+    end if
+    let args = "\"" + version + "\""
+    let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS")
+    let sh_path = path.join_path(scripts_dir, "post-install.sh")
+    if file.fileExists(sh_path)
+        return run_script(pkg_dir, "post-install.sh", args)
+    end if
+    return run_script(pkg_dir, "post-install", args)
+end run_post_install
+
+// Run pre-remove script (passes $VERSION)
+fn run_pre_remove(pkg_dir: string, version: string): bool
+    if not has_scripts(pkg_dir)
+        return true
+    end if
+    let args = "\"" + version + "\""
+    let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS")
+    let sh_path = path.join_path(scripts_dir, "pre-remove.sh")
+    if file.fileExists(sh_path)
+        return run_script(pkg_dir, "pre-remove.sh", args)
+    end if
+    return run_script(pkg_dir, "pre-remove", args)
+end run_pre_remove
+
+// Run post-remove script (passes $VERSION)
+fn run_post_remove(pkg_dir: string, version: string): bool
+    if not has_scripts(pkg_dir)
+        return true
+    end if
+    let args = "\"" + version + "\""
+    let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS")
+    let sh_path = path.join_path(scripts_dir, "post-remove.sh")
+    if file.fileExists(sh_path)
+        return run_script(pkg_dir, "post-remove.sh", args)
+    end if
+    return run_script(pkg_dir, "post-remove", args)
+end run_post_remove
+
+// Run pre-upgrade script (passes $OLD_VERSION $NEW_VERSION)
+fn run_pre_upgrade(pkg_dir: string, old_version: string, new_version: string): bool
+    if not has_scripts(pkg_dir)
+        return true
+    end if
+    let args = "\"" + old_version + "\" \"" + new_version + "\""
+    let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS")
+    let sh_path = path.join_path(scripts_dir, "pre-upgrade.sh")
+    if file.fileExists(sh_path)
+        return run_script(pkg_dir, "pre-upgrade.sh", args)
+    end if
+    return run_script(pkg_dir, "pre-upgrade", args)
+end run_pre_upgrade
+
+// Run post-upgrade script (passes $OLD_VERSION $NEW_VERSION)
+fn run_post_upgrade(pkg_dir: string, old_version: string, new_version: string): bool
+    if not has_scripts(pkg_dir)
+        return true
+    end if
+    let args = "\"" + old_version + "\" \"" + new_version + "\""
+    let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS")
+    let sh_path = path.join_path(scripts_dir, "post-upgrade.sh")
+    if file.fileExists(sh_path)
+        return run_script(pkg_dir, "post-upgrade.sh", args)
+    end if
+    return run_script(pkg_dir, "post-upgrade", args)
+end run_post_upgrade
+
+// Run a script directly from a stored scripts directory
+fn run_stored_script(scripts_dir: string, script_name: string, script_args: string): bool
+    let script_path = path.join_path(scripts_dir, script_name)
+
+    if not file.fileExists(script_path)
+        return true
+    end if
+
+    mut cmd = ""
+
+    // Shell scripts (.sh) - run with zsh
+    if str.ends_with(script_name, ".sh")
+        cmd = "zsh \"" + script_path + "\""
+    else
+        // Default: try to execute directly
+        cmd = "\"" + script_path + "\""
+    end if
+
+    if str.length(script_args) > 0
+        cmd = cmd + " " + script_args
+    end if
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return false
+    end if
+    let exit_code = process.process_wait(pid)
+    return exit_code == 0
+end run_stored_script
+
+// Run pre-remove script from stored scripts directory (passes $VERSION)
+fn run_stored_pre_remove(scripts_dir: string, version: string): bool
+    if not dir.dir_exists(scripts_dir)
+        return true
+    end if
+    let args = "\"" + version + "\""
+    let sh_path = path.join_path(scripts_dir, "pre-remove.sh")
+    if file.fileExists(sh_path)
+        return run_stored_script(scripts_dir, "pre-remove.sh", args)
+    end if
+    return run_stored_script(scripts_dir, "pre-remove", args)
+end run_stored_pre_remove
+
+// Run post-remove script from stored scripts directory (passes $VERSION)
+fn run_stored_post_remove(scripts_dir: string, version: string): bool
+    if not dir.dir_exists(scripts_dir)
+        return true
+    end if
+    let args = "\"" + version + "\""
+    let sh_path = path.join_path(scripts_dir, "post-remove.sh")
+    if file.fileExists(sh_path)
+        return run_stored_script(scripts_dir, "post-remove.sh", args)
+    end if
+    return run_stored_script(scripts_dir, "post-remove", args)
+end run_stored_post_remove
+
+// Run pre-upgrade script from stored scripts directory (passes $OLD_VERSION $NEW_VERSION)
+fn run_stored_pre_upgrade(scripts_dir: string, old_version: string, new_version: string): bool
+    if not dir.dir_exists(scripts_dir)
+        return true
+    end if
+    let args = "\"" + old_version + "\" \"" + new_version + "\""
+    let sh_path = path.join_path(scripts_dir, "pre-upgrade.sh")
+    if file.fileExists(sh_path)
+        return run_stored_script(scripts_dir, "pre-upgrade.sh", args)
+    end if
+    return run_stored_script(scripts_dir, "pre-upgrade", args)
+end run_stored_pre_upgrade
+
+// Run post-upgrade script from stored scripts directory (passes $OLD_VERSION $NEW_VERSION)
+fn run_stored_post_upgrade(scripts_dir: string, old_version: string, new_version: string): bool
+    if not dir.dir_exists(scripts_dir)
+        return true
+    end if
+    let args = "\"" + old_version + "\" \"" + new_version + "\""
+    let sh_path = path.join_path(scripts_dir, "post-upgrade.sh")
+    if file.fileExists(sh_path)
+        return run_stored_script(scripts_dir, "post-upgrade.sh", args)
+    end if
+    return run_stored_script(scripts_dir, "post-upgrade", args)
+end run_stored_post_upgrade
+
+// Read list of config files from .CONFIG file in extracted package
+fn read_config_files(pkg_dir: string, config_files: [string], max_count: int): int
+    let config_path = path.join_path(pkg_dir, ".CONFIG")
+    if not file.fileExists(config_path)
+        return 0
+    end if
+
+    let content = file.readFile(config_path)
+    if str.length(content) == 0
+        return 0
+    end if
+
+    // Split by newlines
+    mut lines: [string] = new [string](max_count)
+    let line_count = str.split(content, '\n', lines, max_count)
+
+    // Filter out empty lines
+    mut count = 0
+    mut i = 0
+    while i < line_count and count < max_count
+        let line = str.trim_ws(lines[i])
+        if str.length(line) > 0 and line[0] != '#'
+            // Normalize path (remove leading ./)
+            if str.starts_with(line, "./")
+                config_files[count] = str.substring(line, 2, str.length(line) - 2)
+            else
+                config_files[count] = line
+            end if
+            count = count + 1
+        end if
+        i = i + 1
+    end while
+
+    return count
+end read_config_files
+
+// Check if a file path is in the config files list
+fn is_config_file(rel_path: string, config_files: [string], config_count: int): bool
+    mut i = 0
+    while i < config_count
+        if str.equals(rel_path, config_files[i])
+            return true
+        end if
+        i = i + 1
+    end while
+    return false
+end is_config_file
+
+// Install a config file with checksum-based protection.
+// manifest_sha256: checksum of the new package's config file
+// stored_sha256: checksum from the previously installed package version (empty on fresh install)
+fn install_config_file(src_path: string, dest_path: string, manifest_sha256: string, stored_sha256: string): bool
+    if not stat.exists(dest_path)
+        // Fresh install — file doesn't exist yet
+        // Create parent directory if needed
+        let dest_dir = path.dirname(dest_path)
+        if str.length(dest_dir) > 0 and not dir.dir_exists(dest_dir)
+            dir.create_dir_all(dest_dir)
+        end if
+
+        return ops.copy_file_preserve(src_path, dest_path)
+    end if
+
+    // File exists — check if user has modified it
+    if str.length(stored_sha256) == 0
+        // No stored checksum (fresh install over existing file or legacy package)
+        // Be safe: install as .pkg-new, preserve user's file
+        let new_path = dest_path + ".pkg-new"
+        return ops.copy_file_preserve(src_path, new_path)
+    end if
+
+    let installed_sha = checksum.sha256_file(dest_path)
+
+    if str.equals(installed_sha, stored_sha256)
+        // User has not modified the config — safe to overwrite
+        return ops.copy_file_preserve(src_path, dest_path)
+    else
+        // User has modified the config — preserve their version
+        let new_path = dest_path + ".pkg-new"
+        return ops.copy_file_preserve(src_path, new_path)
+    end if
+end install_config_file
+
+// Look up sha256 for a path in manifest entries
+fn find_manifest_sha256(rel_path: string, entries: [mtree.ManifestEntry], count: int): string
+    mut i = 0
+    while i < count
+        let epath = mtree.entry_path(entries[i])
+        mut entry_rel = epath
+        if str.starts_with(epath, "./")
+            entry_rel = str.substring(epath, 2, str.length(epath) - 2)
+        end if
+        if str.equals(entry_rel, rel_path)
+            return mtree.entry_sha256(entries[i])
+        end if
+        i = i + 1
+    end while
+    return ""
+end find_manifest_sha256
+
+// Install files from extracted package with old manifest for upgrade config protection
+fn install_files_upgrade(src_dir: string, root: string, old_entries: [mtree.ManifestEntry], old_entry_count: int): bool
+    mut config_files: [string] = new [string](256)
+    let config_count = read_config_files(src_dir, config_files, 256)
+
+    mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192)
+    let entry_count = read_manifest(src_dir, entries, 8192)
+
+    if entry_count == 0
+        return false
+    end if
+
+    mut i = 0
+    while i < entry_count
+        let entry = entries[i]
+        let etype = mtree.entry_type(entry)
+        let epath = mtree.entry_path(entry)
+
+        mut rel_path = epath
+        if str.starts_with(epath, "./")
+            rel_path = str.substring(epath, 2, str.length(epath) - 2)
+        end if
+
+        let dest_path = path.join_path(root, rel_path)
+        let src_path = path.join_path(src_dir, rel_path)
+        let mode = mtree.entry_mode(entry)
+        let uid = mtree.name_to_uid(mtree.entry_uname(entry))
+        let gid = mtree.name_to_gid(mtree.entry_gname(entry))
+
+        if str.equals(etype, "dir")
+            if not dir.dir_exists(dest_path)
+                if not dir.create_dir_all(dest_path)
+                    return false
+                end if
+            end if
+            perm.chmod(dest_path, mode)
+            perm.chown(dest_path, uid, gid)
+
+        elif str.equals(etype, "file")
+            if is_config_file(rel_path, config_files, config_count)
+                let manifest_sha = mtree.entry_sha256(entry)
+                // Look up old sha256 from previous version's manifest
+                let stored_sha = find_manifest_sha256(rel_path, old_entries, old_entry_count)
+                install_config_file(src_path, dest_path, manifest_sha, stored_sha)
+            else
+                let parent = path.dirname(dest_path)
+                if str.length(parent) > 0 and not dir.dir_exists(parent)
+                    dir.create_dir_all(parent)
+                end if
+
+                if stat.is_symlink(dest_path)
+                    ops.remove_file(dest_path)
+                elif stat.exists(dest_path)
+                    ops.remove_file(dest_path)
+                end if
+
+                if not ops.copy_file_preserve(src_path, dest_path)
+                    return false
+                end if
+
+                perm.chmod(dest_path, mode)
+                perm.chown(dest_path, uid, gid)
+
+                let manifest_sha = mtree.entry_sha256(entry)
+                if str.length(manifest_sha) > 0
+                    let installed_sha = checksum.sha256_file(dest_path)
+                    if not str.equals(installed_sha, manifest_sha)
+                        return false
+                    end if
+                end if
+            end if
+
+        elif str.equals(etype, "link")
+            let link_target = mtree.entry_link(entry)
+
+            if stat.is_symlink(dest_path) or stat.exists(dest_path)
+                ops.remove_file(dest_path)
+            end if
+
+            let parent = path.dirname(dest_path)
+            if str.length(parent) > 0 and not dir.dir_exists(parent)
+                dir.create_dir_all(parent)
+            end if
+
+            if not link.symlink(link_target, dest_path)
+                return false
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    return true
+end install_files_upgrade
+
+// Parse a TOML inline array string like ["foo", "bar"] into a string array
+fn parse_toml_array(value: string, result: [string], max_items: int): int
+    let len = str.length(value)
+    if len < 2
+        return 0
+    end if
+
+    if value[0] != '['
+        if len > 0
+            result[0] = value
+            return 1
+        end if
+        return 0
+    end if
+
+    mut pos = 1
+    mut count = 0
+
+    while pos < len and count < max_items
+        while pos < len and (value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n')
+            pos = pos + 1
+        end while
+
+        if pos >= len or value[pos] == ']'
+            break
+        end if
+
+        if value[pos] == '"'
+            pos = pos + 1
+            mut item_start = pos
+            while pos < len and value[pos] != '"'
+                pos = pos + 1
+            end while
+            if pos > item_start
+                result[count] = str.substring(value, item_start, pos - item_start)
+                count = count + 1
+            end if
+            pos = pos + 1
+        elif value[pos] != ']' and value[pos] != ','
+            mut item_start = pos
+            while pos < len and value[pos] != ',' and value[pos] != ']' and value[pos] != ' '
+                pos = pos + 1
+            end while
+            if pos > item_start
+                result[count] = str.substring(value, item_start, pos - item_start)
+                count = count + 1
+            end if
+        end if
+
+        while pos < len and (value[pos] == ',' or value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n')
+            pos = pos + 1
+        end while
+    end while
+
+    return count
+end parse_toml_array
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/core/pkgdb.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/pkgdb.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,1027 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: pkgdb.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: MsgPack-based package database for fast lookups
+
+    The package database provides fast indexed access to:
+    - Installed packages (packages.db)
+    - File ownership (files.db)
+
+    Source of truth remains in /var/lib/coral/installed/<pkg>/pkg.toml
+    These indexes can be rebuilt with 'coral db rebuild'
+
+    Index files are stored as Base64-encoded MsgPack data for compatibility
+    with Reef's string-based file I/O.
+
+******************************************************************************/
+
+module core.pkgdb
+
+import types
+import io.file
+import io.dir
+import io.path
+import core.str
+import core.database
+import encoding.msgpack
+import encoding.base64
+
+export
+    // Package index entry (for packages.db)
+    type PkgIndexEntry
+
+    // Initialize/ensure db directory exists
+    fn init_db(): bool
+
+    // Check if indexes exist and are valid
+    fn indexes_exist(): bool
+
+    // Get package count from index (fast)
+    fn get_package_count(): int
+
+    // List all packages from index (fast)
+    fn list_packages(names: [string], versions: [string], max_count: int): int
+
+    // Check if package is in index
+    fn is_indexed(name: string): bool
+
+    // Get package info from index
+    fn get_indexed_package(name: string): PkgIndexEntry
+
+    // Find file owner from index (fast)
+    fn find_file_owner(file_path: string): string
+
+    // Update index when package is installed
+    fn index_add_package(name: string, version: string, release: int, description: string): bool
+
+    // Update index when package is removed
+    fn index_remove_package(name: string): bool
+
+    // Add files to file index
+    fn index_add_files(pkg_name: string, files: [string], file_count: int): bool
+
+    // Remove files from file index
+    fn index_remove_files(pkg_name: string): bool
+
+    // Rebuild all indexes from installed packages (source of truth)
+    fn rebuild_indexes(): bool
+
+    // Get index statistics
+    fn get_index_stats(): IndexStats
+
+    // Root-aware versions for alternate root
+    fn init_db_rooted(root: string): bool
+    fn indexes_exist_rooted(root: string): bool
+    fn rebuild_indexes_rooted(root: string): bool
+    fn get_index_stats_rooted(root: string): IndexStats
+    fn find_file_owner_rooted(file_path: string, root: string): string
+
+    // Debug: dump files index contents
+    fn dump_files_index_rooted(root: string, paths: [string], owners: [string], max_count: int): int
+
+    type IndexStats
+end export
+
+// ============================================================================
+// Types
+// ============================================================================
+
+type PkgIndexEntry = struct
+    name: string
+    version: string
+    release: int
+    description: string
+    installed_date: string
+    file_count: int
+    found: bool
+end PkgIndexEntry
+
+type IndexStats = struct
+    package_count: int
+    file_count: int
+    packages_db_size: int
+    files_db_size: int
+    last_updated: string
+end IndexStats
+
+// Loaded packages index (deserialized from packages.db)
+type PackagesIndex = struct
+    version: int
+    generated: string
+    package_count: int
+    entries: [PkgIndexEntry]
+    loaded: bool
+end PackagesIndex
+
+// Loaded files index (deserialized from files.db)
+// Uses parallel arrays for binary search capability
+type FilesIndex = struct
+    version: int
+    generated: string
+    file_count: int
+    file_paths: [string]    // Sorted alphabetically for binary search
+    owners: [string]        // Package name at same index as file_paths
+    loaded: bool
+end FilesIndex
+
+// ============================================================================
+// Constants
+// ============================================================================
+
+const INDEX_VERSION: int = 1
+const MAX_PACKAGES: int = 2048
+const MAX_FILES: int = 131072  // 128K files max
+
+// ============================================================================
+// Path Constants
+// ============================================================================
+
+fn db_dir(): string
+    return types.get_index_dir()
+end db_dir
+
+fn packages_db_path(): string
+    return types.get_index_dir() + "/packages.db"
+end packages_db_path
+
+fn files_db_path(): string
+    return types.get_index_dir() + "/files.db"
+end files_db_path
+
+// ============================================================================
+// Buffer Size Estimation Helpers
+// ============================================================================
+
+// Estimate buffer size needed for packages.db
+fn estimate_packages_buffer_size(pkg_count: int): int
+    // Header overhead: ~100 bytes
+    // Per package: ~200 bytes (name, version, description, dates)
+    return 100 + (pkg_count * 200)
+end estimate_packages_buffer_size
+
+// Estimate buffer size needed for files.db
+fn estimate_files_buffer_size(file_count: int): int
+    // Header overhead: ~100 bytes
+    // Per file: ~100 bytes (path + package name)
+    return 100 + (file_count * 100)
+end estimate_files_buffer_size
+
+// Get current timestamp as string
+fn get_timestamp(): string
+    // Simple timestamp - just use a placeholder for now
+    // In production would use sys.time module
+    return "2026-01-29"
+end get_timestamp
+
+// ============================================================================
+// Empty/Default Constructors
+// ============================================================================
+
+fn empty_packages_index(): PackagesIndex
+    return PackagesIndex{
+        version: 0,
+        generated: "",
+        package_count: 0,
+        entries: new [PkgIndexEntry](MAX_PACKAGES),
+        loaded: false
+    }
+end empty_packages_index
+
+fn empty_files_index(): FilesIndex
+    return FilesIndex{
+        version: 0,
+        generated: "",
+        file_count: 0,
+        file_paths: new [string](MAX_FILES),
+        owners: new [string](MAX_FILES),
+        loaded: false
+    }
+end empty_files_index
+
+fn empty_pkg_entry(): PkgIndexEntry
+    return PkgIndexEntry{
+        name: "",
+        version: "",
+        release: 0,
+        description: "",
+        installed_date: "",
+        file_count: 0,
+        found: false
+    }
+end empty_pkg_entry
+
+// Get just the header size for a map (not the entire map contents)
+fn map_header_size(buf: [int], offset: int): int
+    let b = buf[offset] & 255
+    // Fixmap (0x80-0x8F): 1 byte header
+    if (b & 0xF0) == 0x80
+        return 1
+    // map16 (0xDE): 3 byte header
+    elif b == 0xDE
+        return 3
+    // map32 (0xDF): 5 byte header
+    elif b == 0xDF
+        return 5
+    end if
+    return 1  // Default
+end map_header_size
+
+// Get just the header size for an array (not the entire array contents)
+fn array_header_size(buf: [int], offset: int): int
+    let b = buf[offset] & 255
+    // Fixarray (0x90-0x9F): 1 byte header
+    if (b & 0xF0) == 0x90
+        return 1
+    // array16 (0xDC): 3 byte header
+    elif b == 0xDC
+        return 3
+    // array32 (0xDD): 5 byte header
+    elif b == 0xDD
+        return 5
+    end if
+    return 1  // Default
+end array_header_size
+
+// ============================================================================
+// MsgPack Deserialization
+// ============================================================================
+
+// Load packages index from disk
+fn load_packages_index_rooted(root: string): PackagesIndex
+    mut index = empty_packages_index()
+
+    // Check if file exists
+    let db_path = packages_db_path_rooted(root)
+    if not file.fileExists(db_path)
+        return index
+    end if
+
+    // Read base64-encoded content
+    let encoded = file.readFile(db_path)
+    if str.length(encoded) == 0
+        return index
+    end if
+
+    // Decode base64 to buffer
+    let buf_size = str.length(encoded)  // Decoded will be smaller
+    mut buf: [int] = new [int](buf_size)
+    let buf_len = base64.base64_decode_bytes(encoded, buf, buf_size)
+
+    if buf_len == 0
+        return index
+    end if
+
+    // Parse MsgPack structure
+    mut offset = 0
+
+    // Expect map header
+    if not msgpack.msgpack_is_map(buf, offset)
+        return index
+    end if
+
+    let map_len = msgpack.msgpack_unpack_map_len(buf, offset)
+    offset = offset + map_header_size(buf, offset)
+
+    // Iterate through map entries
+    mut entry_idx = 0
+    while entry_idx < map_len and offset < buf_len
+        // Get key
+        let key = msgpack.msgpack_unpack_string(buf, offset)
+        offset = offset + msgpack.msgpack_value_size(buf, offset)
+
+        if str.equals(key, "version")
+            index.version = msgpack.msgpack_unpack_int(buf, offset)
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        elif str.equals(key, "generated")
+            index.generated = msgpack.msgpack_unpack_string(buf, offset)
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        elif str.equals(key, "package_count")
+            index.package_count = msgpack.msgpack_unpack_int(buf, offset)
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        elif str.equals(key, "packages")
+            // Parse packages array
+            let arr_len = msgpack.msgpack_unpack_array_len(buf, offset)
+            offset = offset + array_header_size(buf, offset)
+
+            mut pkg_idx = 0
+            while pkg_idx < arr_len and pkg_idx < MAX_PACKAGES and offset < buf_len
+                // Each package is a map
+                let pkg_map_len = msgpack.msgpack_unpack_map_len(buf, offset)
+                offset = offset + map_header_size(buf, offset)
+
+                mut entry = empty_pkg_entry()
+                entry.found = true
+
+                mut pkg_field = 0
+                while pkg_field < pkg_map_len and offset < buf_len
+                    let field_key = msgpack.msgpack_unpack_string(buf, offset)
+                    offset = offset + msgpack.msgpack_value_size(buf, offset)
+
+                    if str.equals(field_key, "name")
+                        entry.name = msgpack.msgpack_unpack_string(buf, offset)
+                    elif str.equals(field_key, "version")
+                        entry.version = msgpack.msgpack_unpack_string(buf, offset)
+                    elif str.equals(field_key, "release")
+                        entry.release = msgpack.msgpack_unpack_int(buf, offset)
+                    elif str.equals(field_key, "description")
+                        entry.description = msgpack.msgpack_unpack_string(buf, offset)
+                    elif str.equals(field_key, "installed_date")
+                        entry.installed_date = msgpack.msgpack_unpack_string(buf, offset)
+                    elif str.equals(field_key, "file_count")
+                        entry.file_count = msgpack.msgpack_unpack_int(buf, offset)
+                    end if
+                    offset = offset + msgpack.msgpack_value_size(buf, offset)
+
+                    pkg_field = pkg_field + 1
+                end while
+
+                index.entries[pkg_idx] = entry
+                pkg_idx = pkg_idx + 1
+            end while
+        else
+            // Skip unknown key's value
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        end if
+
+        entry_idx = entry_idx + 1
+    end while
+
+    index.loaded = true
+    return index
+end load_packages_index_rooted
+
+// Load files index from disk
+fn load_files_index_rooted(root: string): FilesIndex
+    mut index = empty_files_index()
+
+    // Check if file exists
+    let db_path = files_db_path_rooted(root)
+    if not file.fileExists(db_path)
+        return index
+    end if
+
+    // Read base64-encoded content
+    let encoded = file.readFile(db_path)
+    let encoded_len = str.length(encoded)
+    if encoded_len == 0
+        return index
+    end if
+
+    // Decode base64 to buffer
+    let buf_size = encoded_len  // Decoded will be smaller
+    mut buf: [int] = new [int](buf_size)
+    let buf_len = base64.base64_decode_bytes(encoded, buf, buf_size)
+
+    if buf_len <= 0
+        return index
+    end if
+
+    // Parse MsgPack structure
+    mut offset = 0
+
+    // Expect map header
+    if not msgpack.msgpack_is_map(buf, offset)
+        return index
+    end if
+
+    let map_len = msgpack.msgpack_unpack_map_len(buf, offset)
+    offset = offset + map_header_size(buf, offset)
+
+    // Iterate through map entries
+    mut entry_idx = 0
+    while entry_idx < map_len and offset < buf_len
+        // Get key
+        let key = msgpack.msgpack_unpack_string(buf, offset)
+        offset = offset + msgpack.msgpack_value_size(buf, offset)
+
+        if str.equals(key, "version")
+            index.version = msgpack.msgpack_unpack_int(buf, offset)
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        elif str.equals(key, "generated")
+            index.generated = msgpack.msgpack_unpack_string(buf, offset)
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        elif str.equals(key, "file_count")
+            index.file_count = msgpack.msgpack_unpack_int(buf, offset)
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        elif str.equals(key, "files")
+            // Parse files map
+            let files_map_len = msgpack.msgpack_unpack_map_len(buf, offset)
+            offset = offset + map_header_size(buf, offset)
+
+            mut file_idx = 0
+            while file_idx < files_map_len and file_idx < MAX_FILES and offset < buf_len
+                // Key is file path, value is owner
+                let file_path = msgpack.msgpack_unpack_string(buf, offset)
+                offset = offset + msgpack.msgpack_value_size(buf, offset)
+
+                let owner = msgpack.msgpack_unpack_string(buf, offset)
+                offset = offset + msgpack.msgpack_value_size(buf, offset)
+
+                index.file_paths[file_idx] = file_path
+                index.owners[file_idx] = owner
+                file_idx = file_idx + 1
+            end while
+
+            index.file_count = file_idx
+        else
+            // Skip unknown key's value
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        end if
+
+        entry_idx = entry_idx + 1
+    end while
+
+    index.loaded = true
+    return index
+end load_files_index_rooted
+
+// Binary search for file owner in sorted files index
+// Returns package name or empty string if not found
+fn binary_search_file_owner(index: FilesIndex, path: string): string
+    if not index.loaded or index.file_count == 0
+        return ""
+    end if
+
+    mut low = 0
+    mut high = index.file_count - 1
+
+    while low <= high
+        let mid = (low + high) / 2
+        let cmp = str.compare(path, index.file_paths[mid])
+
+        if cmp == 0
+            return index.owners[mid]
+        elif cmp < 0
+            high = mid - 1
+        else
+            low = mid + 1
+        end if
+    end while
+
+    return ""  // Not found
+end binary_search_file_owner
+
+// Normalize file path for lookup (strip leading /)
+fn normalize_path(path: string): string
+    let len = str.length(path)
+    if len > 0 and path[0] == '/'
+        return str.substring(path, 1, len - 1)
+    end if
+    return path
+end normalize_path
+
+// Linear search fallback for file owner (when binary search fails)
+fn linear_search_file_owner(index: FilesIndex, path: string): string
+    if not index.loaded or index.file_count == 0
+        return ""
+    end if
+
+    mut i = 0
+    while i < index.file_count
+        if str.equals(path, index.file_paths[i])
+            return index.owners[i]
+        end if
+        i = i + 1
+    end while
+
+    return ""
+end linear_search_file_owner
+
+// ============================================================================
+// Initialization
+// ============================================================================
+
+fn init_db(): bool
+    let db = db_dir()
+    if not dir.dir_exists(db)
+        return dir.create_dir_all(db)
+    end if
+    return true
+end init_db
+
+fn indexes_exist(): bool
+    return file.fileExists(packages_db_path()) and file.fileExists(files_db_path())
+end indexes_exist
+
+// ============================================================================
+// Package Index Operations - Uses MsgPack index with fallback
+// ============================================================================
+
+fn get_package_count(): int
+    // Try index first
+    let index = load_packages_index_rooted("")
+    if index.loaded
+        return index.package_count
+    end if
+
+    // Fall back to database scan
+    mut names: [string] = new [string](1024)
+    return database.list_installed(names, 1024)
+end get_package_count
+
+fn list_packages(names: [string], versions: [string], max_count: int): int
+    // Try index first
+    let index = load_packages_index_rooted("")
+    if index.loaded
+        mut count = 0
+        while count < index.package_count and count < max_count
+            names[count] = index.entries[count].name
+            versions[count] = index.entries[count].version
+            count = count + 1
+        end while
+        return count
+    end if
+
+    // Fall back to database scan
+    let count = database.list_installed(names, max_count)
+
+    // Fill in versions
+    mut i = 0
+    while i < count
+        let pkg = database.get_installed(names[i])
+        versions[i] = pkg.info.version
+        i = i + 1
+    end while
+
+    return count
+end list_packages
+
+fn is_indexed(name: string): bool
+    // Try index first
+    let index = load_packages_index_rooted("")
+    if index.loaded
+        mut i = 0
+        while i < index.package_count
+            if str.equals(index.entries[i].name, name)
+                return true
+            end if
+            i = i + 1
+        end while
+        return false
+    end if
+
+    // Fall back to database check
+    return database.is_installed(name)
+end is_indexed
+
+fn get_indexed_package(name: string): PkgIndexEntry
+    // Try index first
+    let index = load_packages_index_rooted("")
+    if index.loaded
+        mut i = 0
+        while i < index.package_count
+            if str.equals(index.entries[i].name, name)
+                return index.entries[i]
+            end if
+            i = i + 1
+        end while
+        return empty_pkg_entry()
+    end if
+
+    // Fall back to database
+    mut entry = empty_pkg_entry()
+
+    if not database.is_installed(name)
+        return entry
+    end if
+
+    let pkg = database.get_installed(name)
+    entry.name = pkg.info.name
+    entry.version = pkg.info.version
+    entry.release = pkg.info.release
+    entry.description = pkg.info.description
+    entry.installed_date = pkg.install_date
+    entry.file_count = pkg.files_count
+    entry.found = true
+
+    return entry
+end get_indexed_package
+
+// ============================================================================
+// File Index Operations - Uses MsgPack index with fallback
+// ============================================================================
+
+fn find_file_owner(file_path: string): string
+    return find_file_owner_rooted(file_path, "")
+end find_file_owner
+
+fn find_file_owner_rooted(file_path: string, root: string): string
+    // Try index first (O(log N) binary search)
+    let index = load_files_index_rooted(root)
+    if index.loaded
+        let normalized = normalize_path(file_path)
+        let owner = binary_search_file_owner(index, normalized)
+        if str.length(owner) > 0
+            return owner
+        end if
+        // Binary search failed - try linear search as fallback
+        // This helps diagnose if the index isn't sorted correctly
+        let linear_owner = linear_search_file_owner(index, normalized)
+        if str.length(linear_owner) > 0
+            return linear_owner
+        end if
+        // File not in index - either not owned or index out of date
+        return ""
+    end if
+
+    // Fall back to database scan (O(N*M))
+    if str.length(root) > 0
+        return database.find_owner_rooted(file_path, root)
+    end if
+    return database.find_owner(file_path)
+end find_file_owner_rooted
+
+// ============================================================================
+// Index Modification (stub - no-op for now)
+// ============================================================================
+
+fn index_add_package(name: string, version: string, release: int, description: string): bool
+    // Stub - no indexing yet
+    return true
+end index_add_package
+
+fn index_remove_package(name: string): bool
+    // Stub - no indexing yet
+    return true
+end index_remove_package
+
+fn index_add_files(pkg_name: string, files: [string], file_count: int): bool
+    // Stub - no indexing yet
+    return true
+end index_add_files
+
+fn index_remove_files(pkg_name: string): bool
+    // Stub - triggers full rebuild
+    return true
+end index_remove_files
+
+// ============================================================================
+// MsgPack Serialization
+// ============================================================================
+
+// Serialize packages index to MsgPack format
+// Returns buffer and sets length via out parameter simulation (length stored at buf[0])
+fn serialize_packages_db_rooted(root: string, buf: [int], max_size: int): int
+    // Get list of installed packages
+    mut names: [string] = new [string](MAX_PACKAGES)
+    let pkg_count = database.list_installed_rooted(names, MAX_PACKAGES, root)
+
+    mut pos = 0
+
+    // Pack map header (4 keys: version, generated, package_count, packages)
+    pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos)
+
+    // Pack "version" -> INDEX_VERSION
+    pos = pos + msgpack.msgpack_pack_string("version", buf, pos)
+    pos = pos + msgpack.msgpack_pack_int(INDEX_VERSION, buf, pos)
+
+    // Pack "generated" -> timestamp
+    pos = pos + msgpack.msgpack_pack_string("generated", buf, pos)
+    pos = pos + msgpack.msgpack_pack_string(get_timestamp(), buf, pos)
+
+    // Pack "package_count" -> count
+    pos = pos + msgpack.msgpack_pack_string("package_count", buf, pos)
+    pos = pos + msgpack.msgpack_pack_int(pkg_count, buf, pos)
+
+    // Pack "packages" -> array of package entries
+    pos = pos + msgpack.msgpack_pack_string("packages", buf, pos)
+    pos = pos + msgpack.msgpack_pack_array_header(pkg_count, buf, pos)
+
+    // Pack each package entry
+    mut i = 0
+    while i < pkg_count and pos < max_size - 1000
+        let pkg = database.get_installed_rooted(names[i], root)
+
+        // Each package is a map with 6 keys
+        pos = pos + msgpack.msgpack_pack_map_header(6, buf, pos)
+
+        // name
+        pos = pos + msgpack.msgpack_pack_string("name", buf, pos)
+        pos = pos + msgpack.msgpack_pack_string(pkg.info.name, buf, pos)
+
+        // version
+        pos = pos + msgpack.msgpack_pack_string("version", buf, pos)
+        pos = pos + msgpack.msgpack_pack_string(pkg.info.version, buf, pos)
+
+        // release
+        pos = pos + msgpack.msgpack_pack_string("release", buf, pos)
+        pos = pos + msgpack.msgpack_pack_int(pkg.info.release, buf, pos)
+
+        // description
+        pos = pos + msgpack.msgpack_pack_string("description", buf, pos)
+        pos = pos + msgpack.msgpack_pack_string(pkg.info.description, buf, pos)
+
+        // installed_date
+        pos = pos + msgpack.msgpack_pack_string("installed_date", buf, pos)
+        pos = pos + msgpack.msgpack_pack_string(pkg.install_date, buf, pos)
+
+        // file_count
+        pos = pos + msgpack.msgpack_pack_string("file_count", buf, pos)
+        pos = pos + msgpack.msgpack_pack_int(pkg.files_count, buf, pos)
+
+        i = i + 1
+    end while
+
+    return pos
+end serialize_packages_db_rooted
+
+// Count total installed files across all packages (cheap — no file content read)
+fn count_total_files_rooted(root: string): int
+    mut pkg_names: [string] = new [string](MAX_PACKAGES)
+    let pkg_count = database.list_installed_rooted(pkg_names, MAX_PACKAGES, root)
+
+    mut total = 0
+    mut i = 0
+    while i < pkg_count
+        mut pkg_files: [string] = new [string](8192)
+        let file_count = database.get_files_rooted(pkg_names[i], pkg_files, 8192, root)
+        total = total + file_count
+        i = i + 1
+    end while
+
+    return total
+end count_total_files_rooted
+
+// Serialize files index to MsgPack format
+// Creates a map of file_path -> package_name for fast ownership lookup
+fn serialize_files_db_rooted(root: string, buf: [int], max_size: int, alloc_size: int): int
+    // Get all installed packages
+    mut pkg_names: [string] = new [string](MAX_PACKAGES)
+    let pkg_count = database.list_installed_rooted(pkg_names, MAX_PACKAGES, root)
+
+    // Collect all files with their owners — sized to actual count + margin
+    mut all_files: [string] = new [string](alloc_size)
+    mut all_owners: [string] = new [string](alloc_size)
+    mut total_files = 0
+
+    mut i = 0
+    while i < pkg_count and total_files < alloc_size - 256
+        mut pkg_files: [string] = new [string](8192)
+        let file_count = database.get_files_rooted(pkg_names[i], pkg_files, 8192, root)
+
+        mut j = 0
+        while j < file_count and total_files < alloc_size
+            all_files[total_files] = pkg_files[j]
+            all_owners[total_files] = pkg_names[i]
+            total_files = total_files + 1
+            j = j + 1
+        end while
+
+        i = i + 1
+    end while
+
+    // Sort files alphabetically for binary search (simple bubble sort for now)
+    sort_file_index(all_files, all_owners, total_files)
+
+    mut pos = 0
+
+    // Pack map header (4 keys: version, generated, file_count, files)
+    pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos)
+
+    // Pack "version" -> INDEX_VERSION
+    pos = pos + msgpack.msgpack_pack_string("version", buf, pos)
+    pos = pos + msgpack.msgpack_pack_int(INDEX_VERSION, buf, pos)
+
+    // Pack "generated" -> timestamp
+    pos = pos + msgpack.msgpack_pack_string("generated", buf, pos)
+    pos = pos + msgpack.msgpack_pack_string(get_timestamp(), buf, pos)
+
+    // Pack "file_count" -> count
+    pos = pos + msgpack.msgpack_pack_string("file_count", buf, pos)
+    pos = pos + msgpack.msgpack_pack_int(total_files, buf, pos)
+
+    // Pack "files" -> map of path -> owner
+    pos = pos + msgpack.msgpack_pack_string("files", buf, pos)
+    pos = pos + msgpack.msgpack_pack_map_header(total_files, buf, pos)
+
+    // Pack each file -> owner pair
+    i = 0
+    while i < total_files and pos < max_size - 500
+        pos = pos + msgpack.msgpack_pack_string(all_files[i], buf, pos)
+        pos = pos + msgpack.msgpack_pack_string(all_owners[i], buf, pos)
+        i = i + 1
+    end while
+
+    return pos
+end serialize_files_db_rooted
+
+// Sort parallel arrays of files and owners alphabetically by file path
+// Uses merge sort for O(n log n) performance on large file counts
+proc sort_file_index(files: [string], owners: [string], count: int)
+    if count <= 1
+        return
+    end if
+
+    // Allocate temporary arrays for merge operations
+    mut tmp_files: [string] = new [string](count)
+    mut tmp_owners: [string] = new [string](count)
+
+    merge_sort(files, owners, tmp_files, tmp_owners, 0, count - 1)
+end sort_file_index
+
+proc merge_sort(files: [string], owners: [string], tmp_files: [string], tmp_owners: [string], left: int, right: int)
+    if left >= right
+        return
+    end if
+
+    let mid = left + (right - left) / 2
+    merge_sort(files, owners, tmp_files, tmp_owners, left, mid)
+    merge_sort(files, owners, tmp_files, tmp_owners, mid + 1, right)
+    merge(files, owners, tmp_files, tmp_owners, left, mid, right)
+end merge_sort
+
+proc merge(files: [string], owners: [string], tmp_files: [string], tmp_owners: [string], left: int, mid: int, right: int)
+    // Copy both halves into temp arrays
+    mut i = left
+    while i <= right
+        tmp_files[i] = files[i]
+        tmp_owners[i] = owners[i]
+        i = i + 1
+    end while
+
+    // Merge back from temp into original
+    mut l = left
+    mut r = mid + 1
+    mut k = left
+
+    while l <= mid and r <= right
+        if str.compare(tmp_files[l], tmp_files[r]) <= 0
+            files[k] = tmp_files[l]
+            owners[k] = tmp_owners[l]
+            l = l + 1
+        else
+            files[k] = tmp_files[r]
+            owners[k] = tmp_owners[r]
+            r = r + 1
+        end if
+        k = k + 1
+    end while
+
+    // Copy remaining left half
+    while l <= mid
+        files[k] = tmp_files[l]
+        owners[k] = tmp_owners[l]
+        l = l + 1
+        k = k + 1
+    end while
+
+    // Right half remaining elements are already in place
+end merge
+
+// ============================================================================
+// Index Rebuild
+// ============================================================================
+
+fn rebuild_indexes(): bool
+    return rebuild_indexes_rooted("")
+end rebuild_indexes
+
+// ============================================================================
+// Index Statistics
+// ============================================================================
+
+fn get_index_stats(): IndexStats
+    mut stats = IndexStats{
+        package_count: 0,
+        file_count: 0,
+        packages_db_size: 0,
+        files_db_size: 0,
+        last_updated: ""
+    }
+
+    // Get package count from source of truth
+    mut names: [string] = new [string](1024)
+    stats.package_count = database.list_installed(names, 1024)
+
+    // Count files across all packages
+    mut i = 0
+    while i < stats.package_count
+        mut files: [string] = new [string](4096)
+        let file_count = database.get_files(names[i], files, 4096)
+        stats.file_count = stats.file_count + file_count
+        i = i + 1
+    end while
+
+    return stats
+end get_index_stats
+
+// ============================================================================
+// Root-aware versions for alternate root
+// ============================================================================
+
+fn db_dir_rooted(root: string): string
+    if str.length(root) == 0
+        return types.get_index_dir()
+    end if
+    return root + types.get_index_dir()
+end db_dir_rooted
+
+fn packages_db_path_rooted(root: string): string
+    return db_dir_rooted(root) + "/packages.db"
+end packages_db_path_rooted
+
+fn files_db_path_rooted(root: string): string
+    return db_dir_rooted(root) + "/files.db"
+end files_db_path_rooted
+
+fn init_db_rooted(root: string): bool
+    let db = db_dir_rooted(root)
+    if not dir.dir_exists(db)
+        return dir.create_dir_all(db)
+    end if
+    return true
+end init_db_rooted
+
+fn indexes_exist_rooted(root: string): bool
+    return file.fileExists(packages_db_path_rooted(root)) and file.fileExists(files_db_path_rooted(root))
+end indexes_exist_rooted
+
+fn rebuild_indexes_rooted(root: string): bool
+    if not init_db_rooted(root)
+        return false
+    end if
+
+    // Serialize packages.db
+    let pkg_buf_size = estimate_packages_buffer_size(MAX_PACKAGES)
+    mut pkg_buf: [int] = new [int](pkg_buf_size)
+    let pkg_len = serialize_packages_db_rooted(root, pkg_buf, pkg_buf_size)
+
+    if pkg_len > 0
+        // Encode to base64 and write
+        let pkg_encoded = base64.base64_encode_bytes(pkg_buf, pkg_len)
+        if not file.writeFile(packages_db_path_rooted(root), pkg_encoded)
+            return false
+        end if
+    end if
+
+    // Serialize files.db — count actual files first to avoid massive over-allocation
+    let actual_file_count = count_total_files_rooted(root)
+    let alloc_size = actual_file_count + 1024
+    let files_buf_size = estimate_files_buffer_size(alloc_size)
+    mut files_buf: [int] = new [int](files_buf_size)
+    let files_len = serialize_files_db_rooted(root, files_buf, files_buf_size, alloc_size)
+
+    if files_len > 0
+        // Encode to base64 and write
+        let files_encoded = base64.base64_encode_bytes(files_buf, files_len)
+        if not file.writeFile(files_db_path_rooted(root), files_encoded)
+            return false
+        end if
+    end if
+
+    return true
+end rebuild_indexes_rooted
+
+fn get_index_stats_rooted(root: string): IndexStats
+    mut stats = IndexStats{
+        package_count: 0,
+        file_count: 0,
+        packages_db_size: 0,
+        files_db_size: 0,
+        last_updated: ""
+    }
+
+    // Get package count from source of truth in alternate root
+    mut names: [string] = new [string](1024)
+    stats.package_count = database.list_installed_rooted(names, 1024, root)
+
+    // Count files across all packages
+    mut i = 0
+    while i < stats.package_count
+        mut files: [string] = new [string](4096)
+        let file_count = database.get_files_rooted(names[i], files, 4096, root)
+        stats.file_count = stats.file_count + file_count
+        i = i + 1
+    end while
+
+    return stats
+end get_index_stats_rooted
+
+// Debug: dump files index contents to arrays
+fn dump_files_index_rooted(root: string, paths: [string], owners: [string], max_count: int): int
+    let index = load_files_index_rooted(root)
+    if not index.loaded
+        return 0
+    end if
+
+    mut count = 0
+    while count < index.file_count and count < max_count
+        paths[count] = index.file_paths[count]
+        owners[count] = index.owners[count]
+        count = count + 1
+    end while
+
+    return count
+end dump_files_index_rooted
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/core/port.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/port.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,588 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: port.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Port loading and parsing from package.toml
+
+******************************************************************************/
+
+module core.port
+
+import types
+import io.file
+import io.dir
+import io.path
+import core.str
+import core.config
+import encoding.toml
+
+export
+    // Load a port from a directory path, returns PortResult
+    fn load(port_path: string): types.PortResult
+
+    // Find a port by name, searches all categories
+    fn find(name: string): types.PortResult
+
+    // List all ports in a category
+    fn list_category(category: string, names: [string], max_count: int): int
+
+    // Get all categories
+    fn get_categories(cats: [string], max_count: int): int
+end export
+
+// Dynamically discover categories by scanning the ports directory
+// Excludes hidden dirs, infrastructure dirs (pkgs/, groups/), and config-aware paths
+fn get_categories(cats: [string], max_count: int): int
+    let cfg = config.load()
+    let ports_dir = config.get_ports_dir(cfg)
+
+    if not dir.dir_exists(ports_dir)
+        return 0
+    end if
+
+    let packages_path = config.get_packages_dir(cfg)
+    let sources_path = config.get_sources_dir(cfg)
+    let build_path = config.get_build_dir(cfg)
+
+    // Extract first path component relative to ports_dir for each configured path
+    let skip1 = get_relative_top_dir(ports_dir, packages_path)
+    let skip2 = get_relative_top_dir(ports_dir, sources_path)
+    let skip3 = get_relative_top_dir(ports_dir, build_path)
+
+    // Also skip the groups directory
+    let groups_dir = types.get_groups_dir()
+    let skip_groups = get_relative_top_dir(ports_dir, groups_dir)
+
+    mut entries: [string] = new [string](256)
+    let entry_count = dir.list_dir(ports_dir, entries, 256)
+
+    mut count = 0
+    mut i = 0
+    while i < entry_count and count < max_count
+        let entry = entries[i]
+        let elen = str.length(entry)
+
+        // Skip hidden directories
+        if elen == 0 or entry[0] == '.'
+            i = i + 1
+            continue
+        end if
+
+        // Skip infrastructure directories
+        if str.equals(entry, skip1) or str.equals(entry, skip2) or str.equals(entry, skip3) or str.equals(entry, skip_groups)
+            i = i + 1
+            continue
+        end if
+
+        // Verify it's actually a directory
+        let entry_path = path.join_path(ports_dir, entry)
+        if dir.dir_exists(entry_path)
+            cats[count] = entry
+            count = count + 1
+        end if
+
+        i = i + 1
+    end while
+
+    return count
+end get_categories
+
+// Extract the first path component of `child` relative to `parent`
+// e.g. parent="/usr/zports", child="/usr/zports/pkgs/packages" -> "pkgs"
+// Returns empty string if child is not under parent
+fn get_relative_top_dir(parent: string, child: string): string
+    let parent_len = str.length(parent)
+    let child_len = str.length(child)
+
+    if child_len <= parent_len
+        return ""
+    end if
+
+    // Check that child starts with parent
+    let prefix = str.substring(child, 0, parent_len)
+    if not str.equals(prefix, parent)
+        return ""
+    end if
+
+    // Skip the separator
+    mut start = parent_len
+    if child[start] == '/'
+        start = start + 1
+    end if
+
+    // Find the next slash or end of string
+    mut end_pos = start
+    while end_pos < child_len
+        if child[end_pos] == '/'
+            break
+        end if
+        end_pos = end_pos + 1
+    end while
+
+    if end_pos > start
+        return str.substring(child, start, end_pos - start)
+    end if
+
+    return ""
+end get_relative_top_dir
+
+// Parse a TOML array value like ["item1", "item2"] into a string array
+// Returns the number of items parsed
+fn parse_toml_array(value: string, result: [string], max_items: int): int
+    let len = str.length(value)
+    if len < 2
+        return 0
+    end if
+
+    // Check for array brackets
+    if value[0] != '['
+        // Single value, not an array
+        if len > 0
+            result[0] = value
+            return 1
+        end if
+        return 0
+    end if
+
+    // Skip the opening bracket
+    mut pos = 1
+    mut count = 0
+
+    while pos < len and count < max_items
+        // Skip whitespace
+        while pos < len and (value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n')
+            pos = pos + 1
+        end while
+
+        if pos >= len or value[pos] == ']'
+            break
+        end if
+
+        // Check for quoted string
+        if value[pos] == '"'
+            pos = pos + 1
+            mut item_start = pos
+            // Find closing quote
+            while pos < len and value[pos] != '"'
+                pos = pos + 1
+            end while
+            if pos > item_start
+                result[count] = str.substring(value, item_start, pos - item_start)
+                count = count + 1
+            end if
+            pos = pos + 1  // Skip closing quote
+        elif value[pos] != ']' and value[pos] != ','
+            // Unquoted value
+            mut item_start = pos
+            while pos < len and value[pos] != ',' and value[pos] != ']' and value[pos] != ' '
+                pos = pos + 1
+            end while
+            if pos > item_start
+                result[count] = str.substring(value, item_start, pos - item_start)
+                count = count + 1
+            end if
+        end if
+
+        // Skip comma and whitespace
+        while pos < len and (value[pos] == ',' or value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n')
+            pos = pos + 1
+        end while
+    end while
+
+    return count
+end parse_toml_array
+
+// Create an empty/default Port structure (delegate to types module)
+fn new_port(): types.Port
+    return types.new_port()
+end new_port
+
+// Helper to create error result
+fn error_result(msg: string): types.PortResult
+    return types.new_port_result(false, new_port(), msg)
+end error_result
+
+// Helper to create success result
+fn success_result(p: types.Port): types.PortResult
+    return types.new_port_result(true, p, "")
+end success_result
+
+// Extract filename from URL
+fn extract_filename_from_url(url: string): string
+    let len = str.length(url)
+    if len == 0
+        return ""
+    end if
+
+    // Find last slash
+    mut last_slash = -1
+    mut i = len - 1
+    while i >= 0
+        if url[i] == '/'
+            last_slash = i
+            break
+        end if
+        i = i - 1
+    end while
+
+    if last_slash < 0
+        return url
+    end if
+
+    let filename = str.substring(url, last_slash + 1, len - last_slash - 1)
+
+    // Remove query string if present
+    let query_len = str.length(filename)
+    mut query_pos = -1
+    i = 0
+    while i < query_len
+        if filename[i] == '?'
+            query_pos = i
+            break
+        end if
+        i = i + 1
+    end while
+
+    if query_pos > 0
+        return str.substring(filename, 0, query_pos)
+    end if
+
+    return filename
+end extract_filename_from_url
+
+// Parse sources from TOML - supports both new [[source]] and legacy [sources] format
+fn parse_sources(keys: [string], vals: [string], entry_count: int): types.SourceInfo
+    mut info = types.new_source_info()
+
+    // Try new [[source]] format first (source.0.file, source.1.file, etc.)
+    mut source_idx = 0
+    mut found_new_format = false
+
+    while source_idx < 16
+        let prefix = "source." + int_to_str(source_idx)
+        let file_key = prefix + ".file"
+
+        if not toml.toml_has_key(keys, vals, entry_count, file_key)
+            break
+        end if
+
+        found_new_format = true
+
+        mut src = types.new_source()
+
+        // Get file name
+        src.file = toml.toml_get(keys, vals, entry_count, file_key)
+
+        // Get URLs array
+        let urls_key = prefix + ".urls"
+        if toml.toml_has_key(keys, vals, entry_count, urls_key)
+            let urls_val = toml.toml_get(keys, vals, entry_count, urls_key)
+            mut urls_arr: [string] = new [string](16)
+            let urls_count = parse_toml_array(urls_val, urls_arr, 16)
+            src.urls = urls_arr
+            src.url_count = urls_count
+        end if
+
+        // Get checksum (single value)
+        let checksum_key = prefix + ".checksum"
+        if toml.toml_has_key(keys, vals, entry_count, checksum_key)
+            src.checksum = toml.toml_get(keys, vals, entry_count, checksum_key)
+        end if
+
+        // Get extract flag (default: true)
+        let extract_key = prefix + ".extract"
+        if toml.toml_has_key(keys, vals, entry_count, extract_key)
+            src.extract = toml.toml_get_bool(keys, vals, entry_count, extract_key)
+        end if
+
+        info.sources[source_idx] = src
+        source_idx = source_idx + 1
+    end while
+
+    if found_new_format
+        info.count = source_idx
+        return info
+    end if
+
+    // Fall back to legacy [sources] format
+    if toml.toml_has_key(keys, vals, entry_count, "sources.urls")
+        let urls_val = toml.toml_get(keys, vals, entry_count, "sources.urls")
+        mut urls_arr: [string] = new [string](16)
+        let urls_count = parse_toml_array(urls_val, urls_arr, 16)
+
+        // Parse checksums
+        mut checksums_arr: [string] = new [string](16)
+        if toml.toml_has_key(keys, vals, entry_count, "sources.checksums")
+            let cksum_val = toml.toml_get(keys, vals, entry_count, "sources.checksums")
+            parse_toml_array(cksum_val, checksums_arr, 16)
+        end if
+
+        // Convert legacy format to new Source structs
+        mut i = 0
+        while i < urls_count
+            mut src = types.new_source()
+            src.file = extract_filename_from_url(urls_arr[i])
+            src.urls[0] = urls_arr[i]
+            src.url_count = 1
+            if str.length(checksums_arr[i]) > 0
+                src.checksum = checksums_arr[i]
+            end if
+            src.extract = true
+            info.sources[i] = src
+            i = i + 1
+        end while
+        info.count = urls_count
+    end if
+
+    return info
+end parse_sources
+
+// Helper: convert int to string (for source index keys)
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut result = ""
+    mut value = n
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    return result
+end int_to_str
+
+// Load a port from a directory containing package.toml
+fn load(port_path: string): types.PortResult
+    // Check if directory exists
+    if not dir.dir_exists(port_path)
+        return error_result("Port directory does not exist: " + port_path)
+    end if
+
+    // Check for package.toml
+    let toml_path = path.join_path(port_path, "package.toml")
+    if not file.fileExists(toml_path)
+        return error_result("package.toml not found in: " + port_path)
+    end if
+
+    // Read the TOML file
+    let content = file.readFile(toml_path)
+    if str.length(content) == 0
+        return error_result("Failed to read package.toml or file is empty")
+    end if
+
+    // Parse TOML
+    let keys = toml.toml_alloc_keys()
+    let vals = toml.toml_alloc_values()
+    let entry_count = toml.toml_parse(content, keys, vals)
+
+    if entry_count == 0
+        return error_result("Failed to parse package.toml")
+    end if
+
+    // Build the Port structure
+    mut port = new_port()
+    port.port_dir = port_path
+
+    // Extract category from path (e.g., /usr/zports/base/vim -> base)
+    let parent_dir = path.dirname(port_path)
+    port.category = path.basename(parent_dir)
+
+    // Parse [package] section
+    port.info.name = toml.toml_get(keys, vals, entry_count, "package.name")
+    port.info.version = toml.toml_get(keys, vals, entry_count, "package.version")
+    port.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release")
+    port.info.description = toml.toml_get(keys, vals, entry_count, "package.description")
+    port.info.url = toml.toml_get(keys, vals, entry_count, "package.url")
+    port.info.license = toml.toml_get(keys, vals, entry_count, "package.license")
+    port.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer")
+
+    // Architecture (default to x86_64 if not specified)
+    let arch_val = toml.toml_get(keys, vals, entry_count, "package.arch")
+    if str.length(arch_val) > 0
+        port.info.arch = arch_val
+    end if
+
+    // Parse alternatives array
+    if toml.toml_has_key(keys, vals, entry_count, "package.alternatives")
+        let alt_val = toml.toml_get(keys, vals, entry_count, "package.alternatives")
+        mut alt_arr: [string] = new [string](32)
+        let alt_count = parse_toml_array(alt_val, alt_arr, 32)
+        port.info.alternatives = alt_arr
+        port.info.alternatives_count = alt_count
+    end if
+
+    // Parse conflicts array
+    if toml.toml_has_key(keys, vals, entry_count, "package.conflicts")
+        let conf_val = toml.toml_get(keys, vals, entry_count, "package.conflicts")
+        mut conf_arr: [string] = new [string](32)
+        let conf_count = parse_toml_array(conf_val, conf_arr, 32)
+        port.info.conflicts = conf_arr
+        port.info.conflicts_count = conf_count
+    end if
+
+    // Parse [dependencies] section
+    if toml.toml_has_key(keys, vals, entry_count, "dependencies.runtime")
+        let rt_val = toml.toml_get(keys, vals, entry_count, "dependencies.runtime")
+        mut rt_arr: [string] = new [string](64)
+        let rt_count = parse_toml_array(rt_val, rt_arr, 64)
+        port.deps.runtime = rt_arr
+        port.deps.runtime_count = rt_count
+    end if
+
+    if toml.toml_has_key(keys, vals, entry_count, "dependencies.build")
+        let bd_val = toml.toml_get(keys, vals, entry_count, "dependencies.build")
+        mut bd_arr: [string] = new [string](64)
+        let bd_count = parse_toml_array(bd_val, bd_arr, 64)
+        port.deps.build = bd_arr
+        port.deps.build_count = bd_count
+    end if
+
+    // Parse sources - try new [[source]] format first, fall back to legacy [sources]
+    port.sources = parse_sources(keys, vals, entry_count)
+
+    // Parse [patches] section
+    if toml.toml_has_key(keys, vals, entry_count, "patches.files")
+        let patch_val = toml.toml_get(keys, vals, entry_count, "patches.files")
+        mut patch_arr: [string] = new [string](32)
+        let patch_count = parse_toml_array(patch_val, patch_arr, 32)
+        port.patches.files = patch_arr
+        port.patches.count = patch_count
+    end if
+
+    if toml.toml_has_key(keys, vals, entry_count, "patches.strip")
+        port.patches.strip = toml.toml_get_int(keys, vals, entry_count, "patches.strip")
+    end if
+
+    // Parse [config] section
+    if toml.toml_has_key(keys, vals, entry_count, "config.files")
+        let cfg_val = toml.toml_get(keys, vals, entry_count, "config.files")
+        mut cfg_arr: [string] = new [string](32)
+        let cfg_count = parse_toml_array(cfg_val, cfg_arr, 32)
+        port.config.files = cfg_arr
+        port.config.count = cfg_count
+    end if
+
+    // Parse [scripts] section
+    if toml.toml_has_key(keys, vals, entry_count, "scripts.pre_install")
+        port.scripts.pre_install = toml.toml_get(keys, vals, entry_count, "scripts.pre_install")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "scripts.post_install")
+        port.scripts.post_install = toml.toml_get(keys, vals, entry_count, "scripts.post_install")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "scripts.pre_remove")
+        port.scripts.pre_remove = toml.toml_get(keys, vals, entry_count, "scripts.pre_remove")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "scripts.post_remove")
+        port.scripts.post_remove = toml.toml_get(keys, vals, entry_count, "scripts.post_remove")
+    end if
+
+    // Parse [paths] section (per-package overrides)
+    if toml.toml_has_key(keys, vals, entry_count, "paths.prefix")
+        port.prefix = toml.toml_get(keys, vals, entry_count, "paths.prefix")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "paths.sysconfdir")
+        port.sysconfdir = toml.toml_get(keys, vals, entry_count, "paths.sysconfdir")
+    end if
+
+    // Parse [build] section
+    if toml.toml_has_key(keys, vals, entry_count, "build.parallel")
+        port.build_cfg.parallel = toml.toml_get_bool(keys, vals, entry_count, "build.parallel")
+    end if
+    if toml.toml_has_key(keys, vals, entry_count, "build.jobs")
+        port.build_cfg.jobs = toml.toml_get_int(keys, vals, entry_count, "build.jobs")
+    end if
+
+    // Validate required fields
+    if str.length(port.info.name) == 0
+        return types.new_port_result(false, port, "package.name is required")
+    end if
+
+    if str.length(port.info.version) == 0
+        return types.new_port_result(false, port, "package.version is required")
+    end if
+
+    return success_result(port)
+end load
+
+// Find a port by name, searching all categories
+// Accepts both "kernel" (searches all categories) and "hammerhead/kernel" (direct category/name)
+fn find(name: string): types.PortResult
+    let cfg = config.load()
+    let ports_dir = config.get_ports_dir(cfg)
+
+    // Check if ports directory exists
+    if not dir.dir_exists(ports_dir)
+        return error_result("Ports directory not found: " + ports_dir + " (check /etc/coral/coral.conf)")
+    end if
+
+    // Check if name contains a slash (category/name format)
+    let slash_idx = str.index_of(name, "/")
+    if slash_idx >= 0
+        let port_path = path.join_path(ports_dir, name)
+        if dir.dir_exists(port_path)
+            return load(port_path)
+        end if
+        return error_result("Port not found: " + name + " (searched " + ports_dir + "/" + name + ")")
+    end if
+
+    // Search all categories for the name
+    mut cats: [string] = new [string](32)
+    let cat_count = get_categories(cats, 32)
+
+    mut i = 0
+    while i < cat_count
+        let port_path = path.join_path(ports_dir, path.join_path(cats[i], name))
+        if dir.dir_exists(port_path)
+            return load(port_path)
+        end if
+        i = i + 1
+    end while
+
+    return error_result("Port not found: " + name + " (searched " + ports_dir + ")")
+end find
+
+// List all ports in a given category
+fn list_category(category: string, names: [string], max_count: int): int
+    let cfg = config.load()
+    let cat_path = path.join_path(config.get_ports_dir(cfg), category)
+
+    if not dir.dir_exists(cat_path)
+        return 0
+    end if
+
+    mut entries: [string] = new [string](256)
+    let entry_count = dir.list_dir(cat_path, entries, 256)
+
+    mut count = 0
+    mut i = 0
+    while i < entry_count and count < max_count
+        let entry = entries[i]
+        // Skip hidden files and non-directories
+        if str.length(entry) > 0 and entry[0] != '.'
+            let entry_path = path.join_path(cat_path, entry)
+            // Check if it's a valid port (has package.toml)
+            let toml_path = path.join_path(entry_path, "package.toml")
+            if file.fileExists(toml_path)
+                names[count] = entry
+                count = count + 1
+            end if
+        end if
+        i = i + 1
+    end while
+
+    return count
+end list_category
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/core/portindex.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/portindex.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,575 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: portindex.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Ports index for fast port lookups
+
+    The ports index provides fast indexed access to port metadata
+    without parsing individual package.toml files from disk.
+
+    Index file: /var/lib/coral/db/ports.db
+    Format: Base64-encoded MsgPack (same as packages.db, files.db)
+
+    Rebuild triggers: coral sync, coral db rebuild, coral db init
+    Manual: coral ports cache rebuild
+
+******************************************************************************/
+
+module core.portindex
+
+import types
+import io.file
+import io.dir
+import io.path
+import core.str
+import core.port
+import core.config
+import encoding.msgpack
+import encoding.base64
+
+export
+    type PortIndexEntry
+    type PortsIndex
+
+    // Rebuild the ports index from the ports tree
+    fn rebuild_port_index(): bool
+
+    // Check if ports.db exists
+    fn port_index_exists(): bool
+
+    // Load the ports index from disk
+    fn load_port_index(): PortsIndex
+
+    // Find a single port by name or category/name
+    fn find_port(name: string): PortIndexEntry
+
+    // Search ports by substring match on name + description
+    fn search_ports(term: string, results: [PortIndexEntry], max_count: int): int
+
+    // List all indexed ports
+    fn list_all_ports(entries: [PortIndexEntry], max_count: int): int
+
+    // Get count of indexed ports
+    fn get_port_count(): int
+
+    // Find ports that depend on a given package name
+    fn find_reverse_deps(name: string, results: [PortIndexEntry], max_count: int): int
+end export
+
+// ============================================================================
+// Types
+// ============================================================================
+
+type PortIndexEntry = struct
+    name: string
+    category: string
+    version: string
+    release: int
+    description: string
+    port_dir: string
+    runtime_deps: [string]
+    runtime_dep_count: int
+    found: bool
+end PortIndexEntry
+
+type PortsIndex = struct
+    version: int
+    generated: string
+    port_count: int
+    entries: [PortIndexEntry]
+    loaded: bool
+end PortsIndex
+
+// ============================================================================
+// Constants
+// ============================================================================
+
+const PORT_INDEX_VERSION: int = 1
+const MAX_PORTS: int = 4096
+
+// ============================================================================
+// Path helpers
+// ============================================================================
+
+fn ports_db_path(): string
+    return types.get_index_dir() + "/ports.db"
+end ports_db_path
+
+// Get current timestamp as string
+fn get_timestamp(): string
+    return "2026-02-19"
+end get_timestamp
+
+// ============================================================================
+// Empty constructors
+// ============================================================================
+
+fn empty_port_index(): PortsIndex
+    return PortsIndex{
+        version: 0,
+        generated: "",
+        port_count: 0,
+        entries: new [PortIndexEntry](MAX_PORTS),
+        loaded: false
+    }
+end empty_port_index
+
+fn empty_port_entry(): PortIndexEntry
+    return PortIndexEntry{
+        name: "",
+        category: "",
+        version: "",
+        release: 0,
+        description: "",
+        port_dir: "",
+        runtime_deps: new [string](64),
+        runtime_dep_count: 0,
+        found: false
+    }
+end empty_port_entry
+
+// ============================================================================
+// Buffer size estimation
+// ============================================================================
+
+fn estimate_buffer_size(port_count: int): int
+    // Header: ~100 bytes
+    // Per port: ~300 bytes (name, category, version, description, port_dir, deps)
+    return 100 + (port_count * 300)
+end estimate_buffer_size
+
+// ============================================================================
+// MsgPack header size helpers (same as pkgdb.reef)
+// ============================================================================
+
+fn map_header_size(buf: [int], offset: int): int
+    let b = buf[offset] & 255
+    if (b & 0xF0) == 0x80
+        return 1
+    elif b == 0xDE
+        return 3
+    elif b == 0xDF
+        return 5
+    end if
+    return 1
+end map_header_size
+
+fn array_header_size(buf: [int], offset: int): int
+    let b = buf[offset] & 255
+    if (b & 0xF0) == 0x90
+        return 1
+    elif b == 0xDC
+        return 3
+    elif b == 0xDD
+        return 5
+    end if
+    return 1
+end array_header_size
+
+// ============================================================================
+// Index existence check
+// ============================================================================
+
+fn port_index_exists(): bool
+    return file.fileExists(ports_db_path())
+end port_index_exists
+
+// ============================================================================
+// Rebuild: scan all ports, serialize to ports.db
+// ============================================================================
+
+fn rebuild_port_index(): bool
+    // Ensure db directory exists
+    let db_dir = types.get_index_dir()
+    if not dir.dir_exists(db_dir)
+        dir.create_dir_all(db_dir)
+    end if
+
+    // Get all categories dynamically
+    mut cats: [string] = new [string](32)
+    let cat_count = port.get_categories(cats, 32)
+
+    // Collect all port entries
+    mut entries: [PortIndexEntry] = new [PortIndexEntry](MAX_PORTS)
+    mut total = 0
+
+    mut i = 0
+    while i < cat_count and total < MAX_PORTS
+        let category = cats[i]
+        mut port_names: [string] = new [string](256)
+        let port_count = port.list_category(category, port_names, 256)
+
+        mut j = 0
+        while j < port_count and total < MAX_PORTS
+            let result = port.load(get_port_path(category, port_names[j]))
+            if result.success
+                let p = result.port
+                mut entry = empty_port_entry()
+                entry.name = p.info.name
+                entry.category = p.category
+                entry.version = p.info.version
+                entry.release = p.info.release
+                entry.description = p.info.description
+                entry.port_dir = p.port_dir
+                entry.found = true
+
+                // Copy runtime deps
+                mut k = 0
+                while k < p.deps.runtime_count and k < 64
+                    entry.runtime_deps[k] = p.deps.runtime[k]
+                    k = k + 1
+                end while
+                entry.runtime_dep_count = p.deps.runtime_count
+
+                entries[total] = entry
+                total = total + 1
+            end if
+            j = j + 1
+        end while
+
+        i = i + 1
+    end while
+
+    // Serialize to MsgPack
+    let buf_size = estimate_buffer_size(total)
+    mut buf: [int] = new [int](buf_size)
+    let buf_len = serialize_port_index(entries, total, buf, buf_size)
+
+    if buf_len == 0
+        return false
+    end if
+
+    // Encode to base64 and write
+    let encoded = base64.base64_encode_bytes(buf, buf_len)
+    return file.writeFile(ports_db_path(), encoded)
+end rebuild_port_index
+
+// Build a port path from category and name
+fn get_port_path(category: string, name: string): string
+    let cfg = config.load()
+    let ports_dir = config.get_ports_dir(cfg)
+    return path.join_path(ports_dir, path.join_path(category, name))
+end get_port_path
+
+// ============================================================================
+// Serialization
+// ============================================================================
+
+fn serialize_port_index(entries: [PortIndexEntry], count: int, buf: [int], max_size: int): int
+    mut pos = 0
+
+    // Pack map header (4 keys: version, generated, port_count, ports)
+    pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos)
+
+    // version
+    pos = pos + msgpack.msgpack_pack_string("version", buf, pos)
+    pos = pos + msgpack.msgpack_pack_int(PORT_INDEX_VERSION, buf, pos)
+
+    // generated
+    pos = pos + msgpack.msgpack_pack_string("generated", buf, pos)
+    pos = pos + msgpack.msgpack_pack_string(get_timestamp(), buf, pos)
+
+    // port_count
+    pos = pos + msgpack.msgpack_pack_string("port_count", buf, pos)
+    pos = pos + msgpack.msgpack_pack_int(count, buf, pos)
+
+    // ports array
+    pos = pos + msgpack.msgpack_pack_string("ports", buf, pos)
+    pos = pos + msgpack.msgpack_pack_array_header(count, buf, pos)
+
+    mut i = 0
+    while i < count and pos < max_size - 2000
+        let entry = entries[i]
+
+        // Each port is a map with 7 keys
+        pos = pos + msgpack.msgpack_pack_map_header(7, buf, pos)
+
+        // name
+        pos = pos + msgpack.msgpack_pack_string("name", buf, pos)
+        pos = pos + msgpack.msgpack_pack_string(entry.name, buf, pos)
+
+        // category
+        pos = pos + msgpack.msgpack_pack_string("category", buf, pos)
+        pos = pos + msgpack.msgpack_pack_string(entry.category, buf, pos)
+
+        // version
+        pos = pos + msgpack.msgpack_pack_string("version", buf, pos)
+        pos = pos + msgpack.msgpack_pack_string(entry.version, buf, pos)
+
+        // release
+        pos = pos + msgpack.msgpack_pack_string("release", buf, pos)
+        pos = pos + msgpack.msgpack_pack_int(entry.release, buf, pos)
+
+        // description
+        pos = pos + msgpack.msgpack_pack_string("description", buf, pos)
+        pos = pos + msgpack.msgpack_pack_string(entry.description, buf, pos)
+
+        // port_dir
+        pos = pos + msgpack.msgpack_pack_string("port_dir", buf, pos)
+        pos = pos + msgpack.msgpack_pack_string(entry.port_dir, buf, pos)
+
+        // runtime_deps (array of strings)
+        pos = pos + msgpack.msgpack_pack_string("runtime_deps", buf, pos)
+        pos = pos + msgpack.msgpack_pack_array_header(entry.runtime_dep_count, buf, pos)
+
+        mut d = 0
+        while d < entry.runtime_dep_count
+            pos = pos + msgpack.msgpack_pack_string(entry.runtime_deps[d], buf, pos)
+            d = d + 1
+        end while
+
+        i = i + 1
+    end while
+
+    return pos
+end serialize_port_index
+
+// ============================================================================
+// Deserialization
+// ============================================================================
+
+fn load_port_index(): PortsIndex
+    mut index = empty_port_index()
+
+    let db_path = ports_db_path()
+    if not file.fileExists(db_path)
+        return index
+    end if
+
+    let encoded = file.readFile(db_path)
+    if str.length(encoded) == 0
+        return index
+    end if
+
+    let buf_size = str.length(encoded)
+    mut buf: [int] = new [int](buf_size)
+    let buf_len = base64.base64_decode_bytes(encoded, buf, buf_size)
+
+    if buf_len == 0
+        return index
+    end if
+
+    mut offset = 0
+
+    // Expect map header
+    if not msgpack.msgpack_is_map(buf, offset)
+        return index
+    end if
+
+    let map_len = msgpack.msgpack_unpack_map_len(buf, offset)
+    offset = offset + map_header_size(buf, offset)
+
+    mut entry_idx = 0
+    while entry_idx < map_len and offset < buf_len
+        let key = msgpack.msgpack_unpack_string(buf, offset)
+        offset = offset + msgpack.msgpack_value_size(buf, offset)
+
+        if str.equals(key, "version")
+            index.version = msgpack.msgpack_unpack_int(buf, offset)
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        elif str.equals(key, "generated")
+            index.generated = msgpack.msgpack_unpack_string(buf, offset)
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        elif str.equals(key, "port_count")
+            index.port_count = msgpack.msgpack_unpack_int(buf, offset)
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        elif str.equals(key, "ports")
+            // Parse ports array
+            let arr_len = msgpack.msgpack_unpack_array_len(buf, offset)
+            offset = offset + array_header_size(buf, offset)
+
+            mut port_idx = 0
+            while port_idx < arr_len and port_idx < MAX_PORTS and offset < buf_len
+                let port_map_len = msgpack.msgpack_unpack_map_len(buf, offset)
+                offset = offset + map_header_size(buf, offset)
+
+                mut entry = empty_port_entry()
+                entry.found = true
+
+                mut port_field = 0
+                while port_field < port_map_len and offset < buf_len
+                    let field_key = msgpack.msgpack_unpack_string(buf, offset)
+                    offset = offset + msgpack.msgpack_value_size(buf, offset)
+
+                    if str.equals(field_key, "name")
+                        entry.name = msgpack.msgpack_unpack_string(buf, offset)
+                        offset = offset + msgpack.msgpack_value_size(buf, offset)
+                    elif str.equals(field_key, "category")
+                        entry.category = msgpack.msgpack_unpack_string(buf, offset)
+                        offset = offset + msgpack.msgpack_value_size(buf, offset)
+                    elif str.equals(field_key, "version")
+                        entry.version = msgpack.msgpack_unpack_string(buf, offset)
+                        offset = offset + msgpack.msgpack_value_size(buf, offset)
+                    elif str.equals(field_key, "release")
+                        entry.release = msgpack.msgpack_unpack_int(buf, offset)
+                        offset = offset + msgpack.msgpack_value_size(buf, offset)
+                    elif str.equals(field_key, "description")
+                        entry.description = msgpack.msgpack_unpack_string(buf, offset)
+                        offset = offset + msgpack.msgpack_value_size(buf, offset)
+                    elif str.equals(field_key, "port_dir")
+                        entry.port_dir = msgpack.msgpack_unpack_string(buf, offset)
+                        offset = offset + msgpack.msgpack_value_size(buf, offset)
+                    elif str.equals(field_key, "runtime_deps")
+                        // Parse deps array
+                        let deps_len = msgpack.msgpack_unpack_array_len(buf, offset)
+                        offset = offset + array_header_size(buf, offset)
+
+                        mut dep_idx = 0
+                        while dep_idx < deps_len and dep_idx < 64 and offset < buf_len
+                            entry.runtime_deps[dep_idx] = msgpack.msgpack_unpack_string(buf, offset)
+                            offset = offset + msgpack.msgpack_value_size(buf, offset)
+                            dep_idx = dep_idx + 1
+                        end while
+                        entry.runtime_dep_count = dep_idx
+                    else
+                        offset = offset + msgpack.msgpack_value_size(buf, offset)
+                    end if
+
+                    port_field = port_field + 1
+                end while
+
+                index.entries[port_idx] = entry
+                port_idx = port_idx + 1
+            end while
+        else
+            offset = offset + msgpack.msgpack_value_size(buf, offset)
+        end if
+
+        entry_idx = entry_idx + 1
+    end while
+
+    index.loaded = true
+    return index
+end load_port_index
+
+// ============================================================================
+// Query functions
+// ============================================================================
+
+// Find a single port by name or category/name
+fn find_port(name: string): PortIndexEntry
+    let index = load_port_index()
+    if not index.loaded
+        return empty_port_entry()
+    end if
+
+    // Check if name contains a slash (category/name format)
+    let slash_idx = str.index_of(name, "/")
+
+    mut i = 0
+    while i < index.port_count
+        let entry = index.entries[i]
+
+        if slash_idx >= 0
+            // Match category/name format
+            let full_name = entry.category + "/" + entry.name
+            // Also check directory-based name (category/dirname)
+            let dir_name = path.basename(entry.port_dir)
+            let full_dir_name = entry.category + "/" + dir_name
+            if str.equals(full_name, name) or str.equals(full_dir_name, name)
+                return entry
+            end if
+        else
+            // Match just the name
+            if str.equals(entry.name, name)
+                return entry
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    return empty_port_entry()
+end find_port
+
+// Search ports by substring match on name + description
+fn search_ports(term: string, results: [PortIndexEntry], max_count: int): int
+    let index = load_port_index()
+    if not index.loaded
+        return 0
+    end if
+
+    let lower_term = str.to_lower(term)
+    mut count = 0
+
+    mut i = 0
+    while i < index.port_count and count < max_count
+        let entry = index.entries[i]
+        let lower_name = str.to_lower(entry.name)
+        let lower_desc = str.to_lower(entry.description)
+
+        if str.contains(lower_name, lower_term) or str.contains(lower_desc, lower_term)
+            results[count] = entry
+            count = count + 1
+        end if
+
+        i = i + 1
+    end while
+
+    return count
+end search_ports
+
+// List all indexed ports
+fn list_all_ports(entries: [PortIndexEntry], max_count: int): int
+    let index = load_port_index()
+    if not index.loaded
+        return 0
+    end if
+
+    mut count = 0
+    while count < index.port_count and count < max_count
+        entries[count] = index.entries[count]
+        count = count + 1
+    end while
+
+    return count
+end list_all_ports
+
+// Get count of indexed ports
+fn get_port_count(): int
+    let index = load_port_index()
+    if index.loaded
+        return index.port_count
+    end if
+    return 0
+end get_port_count
+
+// Find ports that have `name` in their runtime dependencies
+fn find_reverse_deps(name: string, results: [PortIndexEntry], max_count: int): int
+    let index = load_port_index()
+    if not index.loaded
+        return 0
+    end if
+
+    mut count = 0
+
+    mut i = 0
+    while i < index.port_count and count < max_count
+        let entry = index.entries[i]
+
+        mut j = 0
+        while j < entry.runtime_dep_count
+            if str.equals(entry.runtime_deps[j], name)
+                results[count] = entry
+                count = count + 1
+                break
+            end if
+            j = j + 1
+        end while
+
+        i = i + 1
+    end while
+
+    return count
+end find_reverse_deps
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/core/repository.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/repository.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,451 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: repository.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Repository manifest handling for binary package distribution
+
+******************************************************************************/
+
+module core.repository
+
+import core.str
+import io.file
+import io.dir
+import io.path
+import encoding.toml
+import util.http as uhttp
+import util.color
+import types
+
+export
+    type Repository
+    type RepoResult
+    type RemotePackage
+    type RemotePackageResult
+
+    fn load_repo(repo_path: string): RepoResult
+    fn list_repos(repos: [Repository], max_count: int): int
+
+    // Remote package operations
+    fn refresh_repo(repo: Repository): bool
+    fn find_remote_package(name: string): RemotePackageResult
+    fn download_package(pkg: RemotePackage): string
+    fn get_cached_manifest(repo: Repository): string
+end export
+
+// Repository definition
+type Repository = struct
+    name: string
+    url: string
+    priority: int
+    enabled: bool
+    is_signed: bool
+    keyid: string
+end Repository
+
+// Result type for repository operations
+type RepoResult = struct
+    success: bool
+    repo: Repository
+    error: string
+end RepoResult
+
+// Remote package from repository manifest
+type RemotePackage = struct
+    name: string
+    version: string
+    release: int
+    description: string
+    license: string
+    category: string
+    depends: [string]
+    depends_count: int
+    size: int
+    compressed: int
+    filename: string
+    sha256: string
+    signature: string
+    repo_name: string
+    repo_url: string
+end RemotePackage
+
+// Result type for remote package search
+type RemotePackageResult = struct
+    success: bool
+    pkg: RemotePackage
+    error: string
+end RemotePackageResult
+
+// Create empty repository
+fn new_repository(): Repository
+    return Repository{ name: "", url: "", priority: 100, enabled: true, is_signed: false, keyid: "" }
+end new_repository
+
+// Create empty remote package
+fn new_remote_package(): RemotePackage
+    return RemotePackage{
+        name: "",
+        version: "",
+        release: 0,
+        description: "",
+        license: "",
+        category: "",
+        depends: new [string](64),
+        depends_count: 0,
+        size: 0,
+        compressed: 0,
+        filename: "",
+        sha256: "",
+        signature: "",
+        repo_name: "",
+        repo_url: ""
+    }
+end new_remote_package
+
+// Create error result
+fn error_result(msg: string): RepoResult
+    return RepoResult{ success: false, repo: new_repository(), error: msg }
+end error_result
+
+// Create package error result
+fn pkg_error_result(msg: string): RemotePackageResult
+    return RemotePackageResult{ success: false, pkg: new_remote_package(), error: msg }
+end pkg_error_result
+
+// Create package success result
+fn pkg_success_result(pkg: RemotePackage): RemotePackageResult
+    return RemotePackageResult{ success: true, pkg: pkg, error: "" }
+end pkg_success_result
+
+// Load a repository from a .repo file
+fn load_repo(repo_path: string): RepoResult
+    if not file.fileExists(repo_path)
+        return error_result("Repository file not found: " + repo_path)
+    end if
+
+    let content = file.readFile(repo_path)
+    if str.length(content) == 0
+        return error_result("Empty repository file: " + repo_path)
+    end if
+
+    // Parse TOML
+    let keys = toml.toml_alloc_keys()
+    let vals = toml.toml_alloc_values()
+    let entry_count = toml.toml_parse(content, keys, vals)
+
+    if entry_count == 0
+        return error_result("Failed to parse repository file: " + repo_path)
+    end if
+
+    mut repo = new_repository()
+
+    // Read repository metadata
+    repo.name = toml.toml_get(keys, vals, entry_count, "repository.name")
+    repo.url = toml.toml_get(keys, vals, entry_count, "repository.url")
+
+    if toml.toml_has_key(keys, vals, entry_count, "repository.priority")
+        repo.priority = toml.toml_get_int(keys, vals, entry_count, "repository.priority")
+    end if
+
+    if toml.toml_has_key(keys, vals, entry_count, "repository.enabled")
+        repo.enabled = toml.toml_get_bool(keys, vals, entry_count, "repository.enabled")
+    end if
+
+    if toml.toml_has_key(keys, vals, entry_count, "repository.signed")
+        repo.is_signed = toml.toml_get_bool(keys, vals, entry_count, "repository.signed")
+    end if
+
+    if toml.toml_has_key(keys, vals, entry_count, "repository.keyid")
+        repo.keyid = toml.toml_get(keys, vals, entry_count, "repository.keyid")
+    end if
+
+    return RepoResult{ success: true, repo: repo, error: "" }
+end load_repo
+
+// List all configured repositories
+fn list_repos(repos: [Repository], max_count: int): int
+    let repos_dir = "/etc/coral/repos.d"
+
+    if not dir.dir_exists(repos_dir)
+        return 0
+    end if
+
+    mut entries: [string] = new [string](64)
+    let entry_count = dir.list_dir(repos_dir, entries, 64)
+
+    mut count = 0
+    mut i = 0
+    while i < entry_count and count < max_count
+        let entry = entries[i]
+
+        // Check for .repo extension
+        if str.ends_with(entry, ".repo")
+            let repo_path = path.join_path(repos_dir, entry)
+            let result = load_repo(repo_path)
+            if result.success
+                repos[count] = result.repo
+                count = count + 1
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    return count
+end list_repos
+
+// Get the path to cached manifest for a repository
+fn get_cached_manifest(repo: Repository): string
+    let cache_dir = "/var/lib/coral/repos"
+    return path.join_path(cache_dir, repo.name + ".toml")
+end get_cached_manifest
+
+// Refresh repository metadata from remote
+fn refresh_repo(repo: Repository): bool
+    if not repo.enabled
+        return false
+    end if
+
+    if str.length(repo.url) == 0
+        return false
+    end if
+
+    // Build URL to repo.toml
+    mut manifest_url = repo.url
+    if not str.ends_with(manifest_url, "/")
+        manifest_url = manifest_url + "/"
+    end if
+    manifest_url = manifest_url + "repo.toml"
+
+    // Ensure cache directory exists
+    let cache_dir = "/var/lib/coral/repos"
+    if not dir.dir_exists(cache_dir)
+        if not dir.create_dir_all(cache_dir)
+            return false
+        end if
+    end if
+
+    // Download manifest
+    let cache_path = get_cached_manifest(repo)
+    return uhttp.download(manifest_url, cache_path)
+end refresh_repo
+
+// Find a package in all enabled repositories
+fn find_remote_package(name: string): RemotePackageResult
+    // Get list of repositories
+    mut repos: [Repository] = new [Repository](16)
+    let repo_count = list_repos(repos, 16)
+
+    if repo_count == 0
+        return pkg_error_result("No repositories configured")
+    end if
+
+    // Search in each repository (by priority)
+    // TODO: Sort by priority
+    mut i = 0
+    while i < repo_count
+        let repo = repos[i]
+
+        if not repo.enabled
+            i = i + 1
+            continue
+        end if
+
+        let result = find_package_in_repo(name, repo)
+        if result.success
+            return result
+        end if
+
+        i = i + 1
+    end while
+
+    return pkg_error_result("Package not found in any repository: " + name)
+end find_remote_package
+
+// Find a package in a specific repository
+fn find_package_in_repo(name: string, repo: Repository): RemotePackageResult
+    let cache_path = get_cached_manifest(repo)
+
+    // Check if we have cached manifest
+    if not file.fileExists(cache_path)
+        // Try to refresh
+        if not refresh_repo(repo)
+            return pkg_error_result("Failed to fetch repository manifest")
+        end if
+    end if
+
+    // Read cached manifest
+    let content = file.readFile(cache_path)
+    if str.length(content) == 0
+        return pkg_error_result("Empty repository manifest")
+    end if
+
+    // Parse TOML to find the package
+    let keys = toml.toml_alloc_keys()
+    let vals = toml.toml_alloc_values()
+    let entry_count = toml.toml_parse(content, keys, vals)
+
+    if entry_count == 0
+        return pkg_error_result("Failed to parse repository manifest")
+    end if
+
+    // Look for package entry: packages.<name>
+    let pkg_key = "packages." + name
+    if not toml.toml_has_key(keys, vals, entry_count, pkg_key + ".version")
+        return pkg_error_result("Package not found: " + name)
+    end if
+
+    // Build RemotePackage
+    mut pkg = new_remote_package()
+    pkg.name = name
+    pkg.repo_name = repo.name
+    pkg.repo_url = repo.url
+
+    pkg.version = toml.toml_get(keys, vals, entry_count, pkg_key + ".version")
+    pkg.release = toml.toml_get_int(keys, vals, entry_count, pkg_key + ".release")
+    pkg.description = toml.toml_get(keys, vals, entry_count, pkg_key + ".description")
+    pkg.license = toml.toml_get(keys, vals, entry_count, pkg_key + ".license")
+    pkg.category = toml.toml_get(keys, vals, entry_count, pkg_key + ".category")
+    pkg.filename = toml.toml_get(keys, vals, entry_count, pkg_key + ".file")
+    pkg.sha256 = toml.toml_get(keys, vals, entry_count, pkg_key + ".sha256")
+    pkg.signature = toml.toml_get(keys, vals, entry_count, pkg_key + ".signature")
+
+    if toml.toml_has_key(keys, vals, entry_count, pkg_key + ".size")
+        pkg.size = toml.toml_get_int(keys, vals, entry_count, pkg_key + ".size")
+    end if
+
+    if toml.toml_has_key(keys, vals, entry_count, pkg_key + ".compressed")
+        pkg.compressed = toml.toml_get_int(keys, vals, entry_count, pkg_key + ".compressed")
+    end if
+
+    // Parse depends array if present
+    if toml.toml_has_key(keys, vals, entry_count, pkg_key + ".depends")
+        let deps_str = toml.toml_get(keys, vals, entry_count, pkg_key + ".depends")
+        pkg.depends_count = parse_toml_array(deps_str, pkg.depends, 64)
+    end if
+
+    // If filename is empty, construct it
+    if str.length(pkg.filename) == 0
+        pkg.filename = name + "-" + pkg.version + "-" + int_to_str(pkg.release) + ".x86_64.pkg.tar.xz"
+    end if
+
+    return pkg_success_result(pkg)
+end find_package_in_repo
+
+// Download a package to local cache
+fn download_package(pkg: RemotePackage): string
+    if str.length(pkg.filename) == 0
+        color.print_error("Package has no filename")
+        return ""
+    end if
+
+    // Build download URL
+    mut base_url = pkg.repo_url
+    if not str.ends_with(base_url, "/")
+        base_url = base_url + "/"
+    end if
+    let pkg_url = base_url + "packages/" + pkg.filename
+
+    // Ensure packages cache directory exists
+    let cache_dir = types.get_packages_dir()
+    if not dir.dir_exists(cache_dir)
+        if not dir.create_dir_all(cache_dir)
+            color.print_error("Failed to create packages cache directory")
+            return ""
+        end if
+    end if
+
+    // Download to cache
+    let dest_path = path.join_path(cache_dir, pkg.filename)
+
+    // Check if already cached
+    if file.fileExists(dest_path)
+        color.print_info("Using cached: " + pkg.filename)
+        return dest_path
+    end if
+
+    // Download
+    color.print_info("Downloading: " + pkg.filename)
+    if not uhttp.download(pkg_url, dest_path)
+        return ""
+    end if
+
+    return dest_path
+end download_package
+
+// Parse TOML array value into string array
+fn parse_toml_array(value: string, result: [string], max_items: int): int
+    let len = str.length(value)
+    if len < 2
+        return 0
+    end if
+
+    if value[0] != '['
+        return 0
+    end if
+
+    mut count = 0
+    mut i = 1
+    mut in_string = false
+    mut item_start = 0
+
+    while i < len and count < max_items
+        let c = value[i]
+
+        if c == '"' and not in_string
+            in_string = true
+            item_start = i + 1
+        elif c == '"' and in_string
+            let item_len = i - item_start
+            if item_len > 0
+                result[count] = str.substring(value, item_start, item_len)
+                count = count + 1
+            end if
+            in_string = false
+        elif c == ']' and not in_string
+            return count
+        end if
+
+        i = i + 1
+    end while
+
+    return count
+end parse_toml_array
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/core/resolver.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/resolver.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,756 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: resolver.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Dependency resolution for package installation
+
+******************************************************************************/
+
+module core.resolver
+
+import core.str
+import io.file
+import io.path
+import types
+import core.port
+import core.database
+import util.version as ver
+
+export
+    type ResolveResult
+
+    fn resolve(name: string): ResolveResult
+    fn get_install_order(names: [string], name_count: int, result: [string], max_count: int): int
+    fn get_dep_tree(name: string, deps: [string], max_count: int): int
+
+    // Build dependency resolver (resolves both runtime + build deps recursively from source)
+    fn resolve_build_deps(name: string): ResolveResult
+
+    // Root-aware version for alternate root installation
+    fn get_install_order_rooted(names: [string], name_count: int, result: [string], max_count: int, root: string): int
+end export
+
+// Result for alternative resolution
+type AlternativeChoice = struct
+    satisfied: bool       // True if an alternative is already installed
+    provider: string      // The installed provider (if satisfied)
+    options: [string]     // Available options to choose from
+    option_count: int
+    base_dep: string      // The base dependency name
+end AlternativeChoice
+
+// Resolution result
+type ResolveResult = struct
+    success: bool
+    packages: [string]
+    count: int
+    error: string
+end ResolveResult
+
+// Create an empty result
+fn new_result(): ResolveResult
+    mut packages: [string] = new [string](256)
+    return ResolveResult{ success: false, packages: packages, count: 0, error: "" }
+end new_result
+
+// Resolve dependencies for a package
+// Returns ordered list of packages to install (dependencies first)
+fn resolve(name: string): ResolveResult
+    mut result = new_result()
+
+    // Track visited to detect cycles
+    mut visited: [string] = new [string](256)
+    mut visited_count = 0
+
+    // Track resolution order
+    mut order: [string] = new [string](256)
+    mut order_count = 0
+
+    // Resolve recursively
+    let ok = resolve_recursive(name, visited, visited_count, order, order_count)
+
+    if not ok.success
+        result.error = ok.error
+        return result
+    end if
+
+    result.success = true
+    result.packages = ok.packages
+    result.count = ok.count
+    return result
+end resolve
+
+// Internal result for recursive resolution
+type RecurseResult = struct
+    success: bool
+    packages: [string]
+    count: int
+    error: string
+end RecurseResult
+
+fn new_recurse_result(): RecurseResult
+    mut packages: [string] = new [string](256)
+    return RecurseResult{ success: true, packages: packages, count: 0, error: "" }
+end new_recurse_result
+
+fn resolve_recursive(name: string, visited: [string], visited_count: int, order: [string], order_count: int): RecurseResult
+    mut result = new_recurse_result()
+    result.packages = order
+    result.count = order_count
+
+    // Check for cycle
+    if array_contains(visited, visited_count, name)
+        // Already visited - skip
+        result.success = true
+        return result
+    end if
+
+    // Mark as visited
+    visited[visited_count] = name
+    let new_visited_count = visited_count + 1
+
+    // Find the port
+    let port_result = port.find(name)
+    if not port_result.success
+        // Check if already installed
+        if database.is_installed(name)
+            result.success = true
+            return result
+        end if
+
+        // Check if an alternative provider exists
+        let provider = database.find_provider(name)
+        if str.length(provider) > 0
+            // Alternative already installed
+            result.success = true
+            return result
+        end if
+
+        result.success = false
+        result.error = "Port not found: " + name
+        return result
+    end if
+
+    let p = port_result.port
+    let pkg_name = p.info.name
+
+    // Resolve runtime dependencies first
+    mut i = 0
+    while i < p.deps.runtime_count
+        let dep = p.deps.runtime[i]
+
+        // Check if this is an alternative dependency
+        if is_alternative_dep(dep)
+            let alt_choice = resolve_alternative(dep)
+
+            if alt_choice.satisfied
+                // An alternative is already installed, skip
+                i = i + 1
+                continue
+            end if
+
+            // No alternative installed - pick the first option
+            // In a full implementation, we'd prompt the user here
+            if alt_choice.option_count > 0
+                let chosen = alt_choice.options[0]
+                if not array_contains(result.packages, result.count, chosen)
+                    let dep_result = resolve_recursive(chosen, visited, new_visited_count, result.packages, result.count)
+                    if not dep_result.success
+                        return dep_result
+                    end if
+                    result.packages = dep_result.packages
+                    result.count = dep_result.count
+                end if
+            end if
+        else
+            // Regular dependency — may include version constraint (e.g., "libfoo>=1.0")
+            let constraint = ver.parse_dep_constraint(dep)
+            let dep_name = ver.constraint_name(constraint)
+
+            if database.is_installed(dep_name)
+                // Installed — check version constraint if present
+                if ver.has_constraint(constraint)
+                    let installed_pkg = database.get_installed(dep_name)
+                    if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint))
+                        // Installed version doesn't satisfy constraint — needs upgrade
+                        if not array_contains(result.packages, result.count, dep_name)
+                            let dep_result = resolve_recursive(dep_name, visited, new_visited_count, result.packages, result.count)
+                            if not dep_result.success
+                                return dep_result
+                            end if
+                            result.packages = dep_result.packages
+                            result.count = dep_result.count
+                        end if
+                    end if
+                end if
+                // No constraint or constraint satisfied — skip
+            else
+                // Not installed — resolve it
+                if not array_contains(result.packages, result.count, dep_name)
+                    let dep_result = resolve_recursive(dep_name, visited, new_visited_count, result.packages, result.count)
+                    if not dep_result.success
+                        return dep_result
+                    end if
+                    result.packages = dep_result.packages
+                    result.count = dep_result.count
+                end if
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    // Add this package to order (after its dependencies)
+    // Use canonical name from package.toml, not the raw search argument
+    if not array_contains(result.packages, result.count, pkg_name)
+        // Skip if already installed
+        if not database.is_installed(pkg_name)
+            result.packages[result.count] = pkg_name
+            result.count = result.count + 1
+        end if
+    end if
+
+    result.success = true
+    return result
+end resolve_recursive
+
+// Get install order for multiple packages
+fn get_install_order(names: [string], name_count: int, result: [string], max_count: int): int
+    mut order_count = 0
+
+    mut i = 0
+    while i < name_count
+        let name = names[i]
+        let resolve_result = resolve(name)
+
+        if resolve_result.success
+            // Add resolved packages to result
+            mut j = 0
+            while j < resolve_result.count and order_count < max_count
+                let pkg = resolve_result.packages[j]
+                // Avoid duplicates
+                if not array_contains(result, order_count, pkg)
+                    result[order_count] = pkg
+                    order_count = order_count + 1
+                end if
+                j = j + 1
+            end while
+        end if
+
+        i = i + 1
+    end while
+
+    return order_count
+end get_install_order
+
+// Get dependency tree for display (with indentation info)
+fn get_dep_tree(name: string, deps: [string], max_count: int): int
+    mut count = 0
+    collect_deps(name, deps, count, 0, max_count)
+    return count
+end get_dep_tree
+
+// Collect dependencies recursively
+fn collect_deps(name: string, deps: [string], count: int, depth: int, max_count: int): int
+    if depth > 20 or count >= max_count
+        return count
+    end if
+
+    let result = port.find(name)
+    if not result.success
+        return count
+    end if
+
+    let p = result.port
+    mut current_count = count
+
+    mut i = 0
+    while i < p.deps.runtime_count and current_count < max_count
+        let dep = p.deps.runtime[i]
+
+        // Add with depth prefix (number of spaces = depth * 2)
+        let indent = make_indent(depth)
+        deps[current_count] = indent + dep
+        current_count = current_count + 1
+
+        // Recurse
+        current_count = collect_deps(dep, deps, current_count, depth + 1, max_count)
+
+        i = i + 1
+    end while
+
+    return current_count
+end collect_deps
+
+// Create indentation string
+fn make_indent(depth: int): string
+    if depth == 0
+        return ""
+    end if
+
+    mut result = ""
+    mut i = 0
+    while i < depth
+        result = result + "  "
+        i = i + 1
+    end while
+    return result
+end make_indent
+
+// Check if array contains a string
+fn array_contains(arr: [string], count: int, s: string): bool
+    mut i = 0
+    while i < count
+        if arr[i] == s
+            return true
+        end if
+        i = i + 1
+    end while
+    return false
+end array_contains
+
+// Check if a dependency string represents alternatives (format: dep:opt1,opt2,opt3)
+fn is_alternative_dep(dep: string): bool
+    return str.contains(dep, ":")
+end is_alternative_dep
+
+// Parse alternative options from a dependency string
+// Format: base_dep:alt1,alt2,alt3
+// Returns number of alternatives parsed, fills alternatives array
+fn parse_alternatives(dep: string, alternatives: [string], max_count: int): int
+    let colon_pos = str.index_of_char(dep, ':')
+    if colon_pos < 0
+        // Not an alternative, return the single dependency
+        alternatives[0] = dep
+        return 1
+    end if
+
+    // Get the alternatives part after the colon
+    let alts_part = str.substring(dep, colon_pos + 1, str.length(dep) - colon_pos - 1)
+
+    // Split by comma
+    return str.split(alts_part, ',', alternatives, max_count)
+end parse_alternatives
+
+// Get the base dependency name from an alternative string
+fn get_base_dep(dep: string): string
+    let colon_pos = str.index_of_char(dep, ':')
+    if colon_pos < 0
+        return dep
+    end if
+    return str.substring(dep, 0, colon_pos)
+end get_base_dep
+
+// Create empty AlternativeChoice
+fn new_alternative_choice(): AlternativeChoice
+    return AlternativeChoice{
+        satisfied: false,
+        provider: "",
+        options: new [string](16),
+        option_count: 0,
+        base_dep: ""
+    }
+end new_alternative_choice
+
+// Resolve an alternative dependency
+// Returns satisfied=true if any option is already installed
+// Otherwise returns the list of available options
+fn resolve_alternative(dep: string): AlternativeChoice
+    mut choice = new_alternative_choice()
+
+    if not is_alternative_dep(dep)
+        // Regular dependency
+        choice.base_dep = dep
+        choice.options[0] = dep
+        choice.option_count = 1
+
+        // Check if installed
+        if database.is_installed(dep)
+            choice.satisfied = true
+            choice.provider = dep
+        end if
+        return choice
+    end if
+
+    // Parse the alternatives
+    choice.base_dep = get_base_dep(dep)
+    choice.option_count = parse_alternatives(dep, choice.options, 16)
+
+    // Check if any alternative is already installed
+    mut i = 0
+    while i < choice.option_count
+        let opt = choice.options[i]
+        if database.is_installed(opt)
+            choice.satisfied = true
+            choice.provider = opt
+            return choice
+        end if
+        i = i + 1
+    end while
+
+    // Check if any provides this via alternatives
+    let provider = database.find_provider(choice.base_dep)
+    if str.length(provider) > 0
+        choice.satisfied = true
+        choice.provider = provider
+        return choice
+    end if
+
+    return choice
+end resolve_alternative
+
+// ============================================================================
+// Build dependency resolver (resolves both runtime + build deps from source)
+// ============================================================================
+
+// Resolve all dependencies needed to build a package from source.
+// Returns topological order: dependencies first, target package last.
+// Resolves both runtime and build deps recursively.
+// Uses proper cycle detection with separate visited/in_stack arrays.
+fn resolve_build_deps(name: string): ResolveResult
+    mut result = new_result()
+
+    // Permanent marks: fully resolved packages
+    mut visited: [string] = new [string](256)
+    mut visited_count = 0
+
+    // Temporary marks: packages on the current DFS stack (for cycle detection)
+    mut in_stack: [string] = new [string](256)
+    mut stack_count = 0
+
+    // Topological build order
+    mut order: [string] = new [string](256)
+    mut order_count = 0
+
+    let ok = resolve_build_recursive(name, visited, visited_count, in_stack, stack_count, order, order_count)
+
+    if not ok.success
+        result.error = ok.error
+        return result
+    end if
+
+    result.success = true
+    result.packages = ok.packages
+    result.count = ok.count
+    return result
+end resolve_build_deps
+
+fn resolve_build_recursive(name: string, visited: [string], visited_count: int,
+                           in_stack: [string], stack_count: int,
+                           order: [string], order_count: int): RecurseResult
+    mut result = new_recurse_result()
+    result.packages = order
+    result.count = order_count
+
+    // Permanent mark: already fully resolved, skip
+    if array_contains(visited, visited_count, name)
+        result.success = true
+        return result
+    end if
+
+    // Temporary mark: cycle detected
+    if array_contains(in_stack, stack_count, name)
+        result.success = false
+        result.error = "Circular dependency detected: " + name
+        return result
+    end if
+
+    // If already installed, mark visited and skip (don't need to build)
+    if database.is_installed(name)
+        visited[visited_count] = name
+        result.success = true
+        return result
+    end if
+
+    // Find the port (must exist to build from source)
+    let port_result = port.find(name)
+    if not port_result.success
+        // Check if a provider is already installed
+        let provider = database.find_provider(name)
+        if str.length(provider) > 0
+            visited[visited_count] = name
+            result.success = true
+            return result
+        end if
+
+        result.success = false
+        result.error = "Port not found: " + name
+        return result
+    end if
+
+    let p = port_result.port
+    let pkg_name = p.info.name
+
+    // Push onto DFS stack (temporary mark)
+    in_stack[stack_count] = name
+    let new_stack_count = stack_count + 1
+
+    // Mark as visited (permanent mark)
+    visited[visited_count] = name
+    let new_visited_count = visited_count + 1
+
+    // Recurse into RUNTIME deps
+    mut i = 0
+    while i < p.deps.runtime_count
+        let dep = p.deps.runtime[i]
+
+        if is_alternative_dep(dep)
+            let alt_choice = resolve_alternative(dep)
+            if not alt_choice.satisfied and alt_choice.option_count > 0
+                let chosen = alt_choice.options[0]
+                let dep_result = resolve_build_recursive(chosen, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count)
+                if not dep_result.success
+                    return dep_result
+                end if
+                result.packages = dep_result.packages
+                result.count = dep_result.count
+            end if
+        else
+            let constraint = ver.parse_dep_constraint(dep)
+            let dep_name = ver.constraint_name(constraint)
+
+            mut needs_build = false
+            if database.is_installed(dep_name)
+                // Check version constraint
+                if ver.has_constraint(constraint)
+                    let installed_pkg = database.get_installed(dep_name)
+                    if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint))
+                        needs_build = true
+                    end if
+                end if
+            else
+                needs_build = true
+            end if
+
+            if needs_build
+                let dep_result = resolve_build_recursive(dep_name, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count)
+                if not dep_result.success
+                    return dep_result
+                end if
+                result.packages = dep_result.packages
+                result.count = dep_result.count
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    // Recurse into BUILD deps
+    i = 0
+    while i < p.deps.build_count
+        let dep = p.deps.build[i]
+
+        if is_alternative_dep(dep)
+            let alt_choice = resolve_alternative(dep)
+            if not alt_choice.satisfied and alt_choice.option_count > 0
+                let chosen = alt_choice.options[0]
+                let dep_result = resolve_build_recursive(chosen, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count)
+                if not dep_result.success
+                    return dep_result
+                end if
+                result.packages = dep_result.packages
+                result.count = dep_result.count
+            end if
+        else
+            let constraint = ver.parse_dep_constraint(dep)
+            let dep_name = ver.constraint_name(constraint)
+
+            mut needs_build = false
+            if database.is_installed(dep_name)
+                if ver.has_constraint(constraint)
+                    let installed_pkg = database.get_installed(dep_name)
+                    if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint))
+                        needs_build = true
+                    end if
+                end if
+            else
+                needs_build = true
+            end if
+
+            if needs_build
+                let dep_result = resolve_build_recursive(dep_name, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count)
+                if not dep_result.success
+                    return dep_result
+                end if
+                result.packages = dep_result.packages
+                result.count = dep_result.count
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    // Post-order: add this package AFTER all its deps (topological sort)
+    if not array_contains(result.packages, result.count, pkg_name)
+        result.packages[result.count] = pkg_name
+        result.count = result.count + 1
+    end if
+
+    result.success = true
+    return result
+end resolve_build_recursive
+
+// ============================================================================
+// Root-aware resolver functions for alternate root installation
+// ============================================================================
+
+// Internal result for rooted recursive resolution
+type RootedRecurseResult = struct
+    success: bool
+    packages: [string]
+    count: int
+    error: string
+end RootedRecurseResult
+
+fn new_rooted_recurse_result(): RootedRecurseResult
+    mut packages: [string] = new [string](256)
+    return RootedRecurseResult{ success: true, packages: packages, count: 0, error: "" }
+end new_rooted_recurse_result
+
+// Resolve dependencies checking against a specific root
+fn resolve_recursive_rooted(name: string, visited: [string], visited_count: int, order: [string], order_count: int, root: string): RootedRecurseResult
+    mut result = new_rooted_recurse_result()
+    result.packages = order
+    result.count = order_count
+
+    // Check for cycle
+    if array_contains(visited, visited_count, name)
+        result.success = true
+        return result
+    end if
+
+    // Mark as visited
+    visited[visited_count] = name
+    let new_visited_count = visited_count + 1
+
+    // Find the port
+    let port_result = port.find(name)
+    if not port_result.success
+        // Check if already installed in target root
+        if database.is_installed_rooted(name, root)
+            result.success = true
+            return result
+        end if
+
+        // Check if an alternative provider exists in target root
+        // Note: find_provider doesn't have rooted version yet, skip for now
+        result.success = false
+        result.error = "Port not found: " + name
+        return result
+    end if
+
+    let p = port_result.port
+    let pkg_name = p.info.name
+
+    // Resolve runtime dependencies first
+    mut i = 0
+    while i < p.deps.runtime_count
+        let dep = p.deps.runtime[i]
+
+        // Skip alternative deps for now (simplification)
+        if not is_alternative_dep(dep)
+            // Parse version constraint if present
+            let constraint = ver.parse_dep_constraint(dep)
+            let dep_name = ver.constraint_name(constraint)
+
+            if database.is_installed_rooted(dep_name, root)
+                // Check version constraint if present
+                if ver.has_constraint(constraint)
+                    let installed_pkg = database.get_installed_rooted(dep_name, root)
+                    if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint))
+                        // Needs upgrade
+                        let dep_result = resolve_recursive_rooted(dep_name, visited, new_visited_count, result.packages, result.count, root)
+                        if not dep_result.success
+                            return dep_result
+                        end if
+                        result.packages = dep_result.packages
+                        result.count = dep_result.count
+                    end if
+                end if
+            else
+                let dep_result = resolve_recursive_rooted(dep_name, visited, new_visited_count, result.packages, result.count, root)
+                if not dep_result.success
+                    return dep_result
+                end if
+                result.packages = dep_result.packages
+                result.count = dep_result.count
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    // Add this package to order (after its dependencies)
+    // Use canonical name from package.toml, not the raw search argument
+    if not array_contains(result.packages, result.count, pkg_name)
+        // Skip if already installed in target root
+        if not database.is_installed_rooted(pkg_name, root)
+            result.packages[result.count] = pkg_name
+            result.count = result.count + 1
+        end if
+    end if
+
+    result.success = true
+    return result
+end resolve_recursive_rooted
+
+// Resolve for a single package with root support
+fn resolve_rooted(name: string, root: string): ResolveResult
+    mut result = new_result()
+
+    mut visited: [string] = new [string](256)
+    mut visited_count = 0
+
+    mut order: [string] = new [string](256)
+    mut order_count = 0
+
+    let ok = resolve_recursive_rooted(name, visited, visited_count, order, order_count, root)
+
+    if not ok.success
+        result.error = ok.error
+        return result
+    end if
+
+    result.success = true
+    result.packages = ok.packages
+    result.count = ok.count
+    return result
+end resolve_rooted
+
+// Get install order for multiple packages, checking against target root
+fn get_install_order_rooted(names: [string], name_count: int, result: [string], max_count: int, root: string): int
+    mut order_count = 0
+
+    mut i = 0
+    while i < name_count
+        let name = names[i]
+        let resolve_result = resolve_rooted(name, root)
+
+        if resolve_result.success
+            mut j = 0
+            while j < resolve_result.count and order_count < max_count
+                let pkg = resolve_result.packages[j]
+                if not array_contains(result, order_count, pkg)
+                    result[order_count] = pkg
+                    order_count = order_count + 1
+                end if
+                j = j + 1
+            end while
+        end if
+
+        i = i + 1
+    end while
+
+    return order_count
+end get_install_order_rooted
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/core/signing.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/signing.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,392 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: signing.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Ed25519 signing and verification for packages and repositories
+
+******************************************************************************/
+
+module core.signing
+
+import core.str
+import io.file
+import io.dir
+import io.path
+import sys.process
+
+export
+    type KeyPair
+    type SignResult
+    type VerifyResult
+
+    fn generate_keypair(name: string): SignResult
+    fn sign_file(file_path: string, key_path: string): SignResult
+    fn verify_file(file_path: string, sig_path: string, key_path: string): VerifyResult
+    fn import_key(key_data: string, name: string): SignResult
+    fn list_keys(keys: [KeyPair], max_count: int): int
+    fn get_keyring_dir(): string
+    fn get_trusted_keys_dir(): string
+
+    // High-level verification functions
+    fn verify_package(pkg_path: string, sig_path: string): VerifyResult
+    fn verify_manifest(manifest_path: string): VerifyResult
+    fn is_key_trusted(keyid: string): bool
+end export
+
+// Key pair information
+type KeyPair = struct
+    name: string
+    keyid: string
+    public_key: string
+    private_key: string
+    created: string
+    trusted: bool
+end KeyPair
+
+// Result for signing operations
+type SignResult = struct
+    success: bool
+    signature: string
+    error: string
+end SignResult
+
+// Result for verification
+type VerifyResult = struct
+    success: bool
+    valid: bool
+    keyid: string
+    error: string
+end VerifyResult
+
+// Get the keyring directory
+fn get_keyring_dir(): string
+    return "/etc/coral/keys"
+end get_keyring_dir
+
+// Create empty keypair
+fn new_keypair(): KeyPair
+    return KeyPair{ name: "", keyid: "", public_key: "", private_key: "", created: "", trusted: false }
+end new_keypair
+
+// Create error result
+fn error_sign_result(msg: string): SignResult
+    return SignResult{ success: false, signature: "", error: msg }
+end error_sign_result
+
+fn error_verify_result(msg: string): VerifyResult
+    return VerifyResult{ success: false, valid: false, keyid: "", error: msg }
+end error_verify_result
+
+// Generate a new Ed25519 keypair
+// Note: This requires an external tool (openssl or signify)
+fn generate_keypair(name: string): SignResult
+    let keyring = get_keyring_dir()
+
+    // Ensure keyring directory exists
+    if not dir.dir_exists(keyring)
+        if not dir.create_dir_all(keyring)
+            return error_sign_result("Failed to create keyring directory")
+        end if
+    end if
+
+    let private_path = path.join_path(keyring, name + ".key")
+    let public_path = path.join_path(keyring, name + ".pub")
+
+    // Check if key already exists
+    if file.fileExists(private_path)
+        return error_sign_result("Key already exists: " + name)
+    end if
+
+    // Generate key using openssl
+    let cmd = "openssl genpkey -algorithm ed25519 -out \"" + private_path + "\" && " +
+              "openssl pkey -in \"" + private_path + "\" -pubout -out \"" + public_path + "\""
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return error_sign_result("Failed to start key generation")
+    end if
+
+    let exit_code = process.process_wait(pid)
+    if exit_code != 0
+        return error_sign_result("Key generation failed")
+    end if
+
+    return SignResult{ success: true, signature: "", error: "" }
+end generate_keypair
+
+// Sign a file with Ed25519
+fn sign_file(file_path: string, key_path: string): SignResult
+    if not file.fileExists(file_path)
+        return error_sign_result("File not found: " + file_path)
+    end if
+
+    if not file.fileExists(key_path)
+        return error_sign_result("Key not found: " + key_path)
+    end if
+
+    let sig_path = file_path + ".sig"
+
+    // Sign using openssl
+    let cmd = "openssl pkeyutl -sign -inkey \"" + key_path + "\" -in \"" + file_path + "\" -out \"" + sig_path + "\""
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return error_sign_result("Failed to start signing")
+    end if
+
+    let exit_code = process.process_wait(pid)
+    if exit_code != 0
+        return error_sign_result("Signing failed")
+    end if
+
+    return SignResult{ success: true, signature: sig_path, error: "" }
+end sign_file
+
+// Verify a file signature
+fn verify_file(file_path: string, sig_path: string, key_path: string): VerifyResult
+    if not file.fileExists(file_path)
+        return error_verify_result("File not found: " + file_path)
+    end if
+
+    if not file.fileExists(sig_path)
+        return error_verify_result("Signature not found: " + sig_path)
+    end if
+
+    if not file.fileExists(key_path)
+        return error_verify_result("Key not found: " + key_path)
+    end if
+
+    // Verify using openssl
+    let cmd = "openssl pkeyutl -verify -pubin -inkey \"" + key_path + "\" -in \"" + file_path + "\" -sigfile \"" + sig_path + "\""
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return error_verify_result("Failed to start verification")
+    end if
+
+    let exit_code = process.process_wait(pid)
+
+    if exit_code == 0
+        return VerifyResult{ success: true, valid: true, keyid: "", error: "" }
+    else
+        return VerifyResult{ success: true, valid: false, keyid: "", error: "Signature verification failed" }
+    end if
+end verify_file
+
+// Import a public key
+fn import_key(key_data: string, name: string): SignResult
+    let keyring = get_keyring_dir()
+
+    if not dir.dir_exists(keyring)
+        if not dir.create_dir_all(keyring)
+            return error_sign_result("Failed to create keyring directory")
+        end if
+    end if
+
+    let key_path = path.join_path(keyring, name + ".pub")
+
+    if file.fileExists(key_path)
+        return error_sign_result("Key already exists: " + name)
+    end if
+
+    if not file.writeFile(key_path, key_data)
+        return error_sign_result("Failed to write key file")
+    end if
+
+    return SignResult{ success: true, signature: "", error: "" }
+end import_key
+
+// List all keys in keyring
+fn list_keys(keys: [KeyPair], max_count: int): int
+    let keyring = get_keyring_dir()
+
+    if not dir.dir_exists(keyring)
+        return 0
+    end if
+
+    mut entries: [string] = new [string](64)
+    let entry_count = dir.list_dir(keyring, entries, 64)
+
+    mut count = 0
+    mut i = 0
+    while i < entry_count and count < max_count
+        let entry = entries[i]
+
+        // Look for .pub files
+        if str.ends_with(entry, ".pub")
+            let name_len = str.length(entry) - 4
+            let name = str.substring(entry, 0, name_len)
+
+            mut kp = new_keypair()
+            kp.name = name
+            kp.trusted = true  // Local keys are trusted
+
+            // Read public key
+            let pub_path = path.join_path(keyring, entry)
+            kp.public_key = file.readFile(pub_path)
+
+            // Check for private key
+            let priv_path = path.join_path(keyring, name + ".key")
+            if file.fileExists(priv_path)
+                kp.private_key = "(present)"
+            end if
+
+            keys[count] = kp
+            count = count + 1
+        end if
+
+        i = i + 1
+    end while
+
+    return count
+end list_keys
+
+// Get the trusted keys directory
+fn get_trusted_keys_dir(): string
+    return "/var/lib/coral/trusted-keys"
+end get_trusted_keys_dir
+
+// Check if a key is in the trusted keys list
+fn is_key_trusted(keyid: string): bool
+    let trusted_dir = get_trusted_keys_dir()
+
+    if not dir.dir_exists(trusted_dir)
+        return false
+    end if
+
+    // Look for key file by keyid
+    let key_path = path.join_path(trusted_dir, keyid + ".pub")
+    if file.fileExists(key_path)
+        return true
+    end if
+
+    // Also check the main keyring
+    let keyring_path = path.join_path(get_keyring_dir(), keyid + ".pub")
+    return file.fileExists(keyring_path)
+end is_key_trusted
+
+// Get path to a trusted key by keyid
+fn get_trusted_key_path(keyid: string): string
+    // Try trusted-keys directory first
+    let trusted_dir = get_trusted_keys_dir()
+    let trusted_path = path.join_path(trusted_dir, keyid + ".pub")
+    if file.fileExists(trusted_path)
+        return trusted_path
+    end if
+
+    // Fall back to main keyring
+    let keyring_path = path.join_path(get_keyring_dir(), keyid + ".pub")
+    if file.fileExists(keyring_path)
+        return keyring_path
+    end if
+
+    return ""
+end get_trusted_key_path
+
+// Verify a package with any trusted key
+fn verify_package(pkg_path: string, sig_path: string): VerifyResult
+    if not file.fileExists(pkg_path)
+        return error_verify_result("Package not found: " + pkg_path)
+    end if
+
+    if not file.fileExists(sig_path)
+        return error_verify_result("Signature not found: " + sig_path)
+    end if
+
+    // Try to verify with each trusted key
+    let trusted_dir = get_trusted_keys_dir()
+    let keyring_dir = get_keyring_dir()
+
+    // Collect all public keys from both directories
+    mut all_keys: [string] = new [string](64)
+    mut key_count = 0
+
+    // Get keys from trusted-keys directory
+    if dir.dir_exists(trusted_dir)
+        mut entries: [string] = new [string](32)
+        let entry_count = dir.list_dir(trusted_dir, entries, 32)
+
+        mut i = 0
+        while i < entry_count and key_count < 64
+            if str.ends_with(entries[i], ".pub")
+                all_keys[key_count] = path.join_path(trusted_dir, entries[i])
+                key_count = key_count + 1
+            end if
+            i = i + 1
+        end while
+    end if
+
+    // Get keys from keyring directory
+    if dir.dir_exists(keyring_dir)
+        mut entries: [string] = new [string](32)
+        let entry_count = dir.list_dir(keyring_dir, entries, 32)
+
+        mut i = 0
+        while i < entry_count and key_count < 64
+            if str.ends_with(entries[i], ".pub")
+                let key_path = path.join_path(keyring_dir, entries[i])
+                // Avoid duplicates
+                mut is_dup = false
+                mut j = 0
+                while j < key_count
+                    if str.equals(all_keys[j], key_path)
+                        is_dup = true
+                    end if
+                    j = j + 1
+                end while
+                if not is_dup
+                    all_keys[key_count] = key_path
+                    key_count = key_count + 1
+                end if
+            end if
+            i = i + 1
+        end while
+    end if
+
+    if key_count == 0
+        return error_verify_result("No trusted keys found")
+    end if
+
+    // Try each key
+    mut i = 0
+    while i < key_count
+        let key_path = all_keys[i]
+        let result = verify_file(pkg_path, sig_path, key_path)
+
+        if result.success and result.valid
+            // Extract keyid from path
+            let filename = path.basename(key_path)
+            let keyid = str.substring(filename, 0, str.length(filename) - 4)
+            return VerifyResult{ success: true, valid: true, keyid: keyid, error: "" }
+        end if
+
+        i = i + 1
+    end while
+
+    return VerifyResult{ success: true, valid: false, keyid: "", error: "Signature does not match any trusted key" }
+end verify_package
+
+// Verify a repository manifest (repo.toml and repo.toml.sig)
+fn verify_manifest(manifest_path: string): VerifyResult
+    if not file.fileExists(manifest_path)
+        return error_verify_result("Manifest not found: " + manifest_path)
+    end if
+
+    let sig_path = manifest_path + ".sig"
+    if not file.fileExists(sig_path)
+        return error_verify_result("Manifest signature not found: " + sig_path)
+    end if
+
+    return verify_package(manifest_path, sig_path)
+end verify_manifest
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/exitcodes.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/exitcodes.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,277 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: exitcodes.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Exit code constants for Coral package manager
+
+******************************************************************************/
+
+module exitcodes
+
+export
+    // Success
+    fn EXIT_SUCCESS(): int
+
+    // General errors (1-9)
+    fn EXIT_ERROR(): int
+    fn EXIT_USAGE(): int
+    fn EXIT_CANCELLED(): int
+
+    // Package errors (10-19)
+    fn EXIT_PKG_NOT_FOUND(): int
+    fn EXIT_PKG_ALREADY_INSTALLED(): int
+    fn EXIT_PKG_INVALID(): int
+    fn EXIT_PKG_CHECKSUM_MISMATCH(): int
+    fn EXIT_PKG_SIGNATURE_INVALID(): int
+    fn EXIT_PKG_CONFLICT(): int
+    fn EXIT_INSTALL_FAILED(): int
+    fn EXIT_REMOVE_FAILED(): int
+    fn EXIT_UPGRADE_FAILED(): int
+
+    // Build errors (20-29)
+    fn EXIT_BUILD_FAILED(): int
+    fn EXIT_PORT_NOT_FOUND(): int
+    fn EXIT_SOURCE_MISSING(): int
+    fn EXIT_PATCH_FAILED(): int
+    fn EXIT_EXTRACT_FAILED(): int
+    fn EXIT_CHECKSUM_FAILED(): int
+    fn EXIT_PACKAGE_FAILED(): int
+
+    // Network errors (30-39)
+    fn EXIT_DOWNLOAD_FAILED(): int
+    fn EXIT_REPO_UNREACHABLE(): int
+    fn EXIT_TIMEOUT(): int
+
+    // Database errors (40-49)
+    fn EXIT_DB_ERROR(): int
+    fn EXIT_DB_REGISTER_FAILED(): int
+    fn EXIT_DB_CORRUPT(): int
+    fn EXIT_DB_INDEX_STALE(): int
+
+    // Permission errors (50-59)
+    fn EXIT_PERMISSION_DENIED(): int
+    fn EXIT_ROOT_REQUIRED(): int
+
+    // Dependency errors (60-69)
+    fn EXIT_DEPS_UNMET(): int
+    fn EXIT_DEPS_CIRCULAR(): int
+    fn EXIT_DEPS_CONFLICT(): int
+    fn EXIT_DEPS_BLOCKED(): int
+
+    // Config errors (70-79)
+    fn EXIT_CONFIG_ERROR(): int
+    fn EXIT_CONFIG_INVALID(): int
+    fn EXIT_CONFIG_MISSING(): int
+
+    // Informational codes for scripting (100+)
+    fn EXIT_UPDATE_AVAILABLE(): int
+    fn EXIT_MANIFEST_STALE(): int
+    fn EXIT_NO_UPDATES(): int
+end export
+
+// ============================================================================
+// Success (0)
+// ============================================================================
+
+fn EXIT_SUCCESS(): int
+    return 0
+end EXIT_SUCCESS
+
+// ============================================================================
+// General Errors (1-9)
+// ============================================================================
+
+fn EXIT_ERROR(): int
+    return 1
+end EXIT_ERROR
+
+fn EXIT_USAGE(): int
+    return 2
+end EXIT_USAGE
+
+fn EXIT_CANCELLED(): int
+    return 3
+end EXIT_CANCELLED
+
+// ============================================================================
+// Package Errors (10-19)
+// ============================================================================
+
+fn EXIT_PKG_NOT_FOUND(): int
+    return 10
+end EXIT_PKG_NOT_FOUND
+
+fn EXIT_PKG_ALREADY_INSTALLED(): int
+    return 11
+end EXIT_PKG_ALREADY_INSTALLED
+
+fn EXIT_PKG_INVALID(): int
+    return 12
+end EXIT_PKG_INVALID
+
+fn EXIT_PKG_CHECKSUM_MISMATCH(): int
+    return 13
+end EXIT_PKG_CHECKSUM_MISMATCH
+
+fn EXIT_PKG_SIGNATURE_INVALID(): int
+    return 14
+end EXIT_PKG_SIGNATURE_INVALID
+
+fn EXIT_PKG_CONFLICT(): int
+    return 15
+end EXIT_PKG_CONFLICT
+
+fn EXIT_INSTALL_FAILED(): int
+    return 16
+end EXIT_INSTALL_FAILED
+
+fn EXIT_REMOVE_FAILED(): int
+    return 17
+end EXIT_REMOVE_FAILED
+
+fn EXIT_UPGRADE_FAILED(): int
+    return 18
+end EXIT_UPGRADE_FAILED
+
+// ============================================================================
+// Build Errors (20-29)
+// ============================================================================
+
+fn EXIT_BUILD_FAILED(): int
+    return 20
+end EXIT_BUILD_FAILED
+
+fn EXIT_PORT_NOT_FOUND(): int
+    return 21
+end EXIT_PORT_NOT_FOUND
+
+fn EXIT_SOURCE_MISSING(): int
+    return 22
+end EXIT_SOURCE_MISSING
+
+fn EXIT_PATCH_FAILED(): int
+    return 23
+end EXIT_PATCH_FAILED
+
+fn EXIT_EXTRACT_FAILED(): int
+    return 24
+end EXIT_EXTRACT_FAILED
+
+fn EXIT_CHECKSUM_FAILED(): int
+    return 25
+end EXIT_CHECKSUM_FAILED
+
+fn EXIT_PACKAGE_FAILED(): int
+    return 26
+end EXIT_PACKAGE_FAILED
+
+// ============================================================================
+// Network Errors (30-39)
+// ============================================================================
+
+fn EXIT_DOWNLOAD_FAILED(): int
+    return 30
+end EXIT_DOWNLOAD_FAILED
+
+fn EXIT_REPO_UNREACHABLE(): int
+    return 31
+end EXIT_REPO_UNREACHABLE
+
+fn EXIT_TIMEOUT(): int
+    return 32
+end EXIT_TIMEOUT
+
+// ============================================================================
+// Database Errors (40-49)
+// ============================================================================
+
+fn EXIT_DB_ERROR(): int
+    return 40
+end EXIT_DB_ERROR
+
+fn EXIT_DB_REGISTER_FAILED(): int
+    return 41
+end EXIT_DB_REGISTER_FAILED
+
+fn EXIT_DB_CORRUPT(): int
+    return 42
+end EXIT_DB_CORRUPT
+
+fn EXIT_DB_INDEX_STALE(): int
+    return 43
+end EXIT_DB_INDEX_STALE
+
+// ============================================================================
+// Permission Errors (50-59)
+// ============================================================================
+
+fn EXIT_PERMISSION_DENIED(): int
+    return 50
+end EXIT_PERMISSION_DENIED
+
+fn EXIT_ROOT_REQUIRED(): int
+    return 51
+end EXIT_ROOT_REQUIRED
+
+// ============================================================================
+// Dependency Errors (60-69)
+// ============================================================================
+
+fn EXIT_DEPS_UNMET(): int
+    return 60
+end EXIT_DEPS_UNMET
+
+fn EXIT_DEPS_CIRCULAR(): int
+    return 61
+end EXIT_DEPS_CIRCULAR
+
+fn EXIT_DEPS_CONFLICT(): int
+    return 62
+end EXIT_DEPS_CONFLICT
+
+fn EXIT_DEPS_BLOCKED(): int
+    return 63
+end EXIT_DEPS_BLOCKED
+
+// ============================================================================
+// Config Errors (70-79)
+// ============================================================================
+
+fn EXIT_CONFIG_ERROR(): int
+    return 70
+end EXIT_CONFIG_ERROR
+
+fn EXIT_CONFIG_INVALID(): int
+    return 71
+end EXIT_CONFIG_INVALID
+
+fn EXIT_CONFIG_MISSING(): int
+    return 72
+end EXIT_CONFIG_MISSING
+
+// ============================================================================
+// Informational Codes for Scripting (100+)
+// ============================================================================
+
+fn EXIT_UPDATE_AVAILABLE(): int
+    return 100
+end EXIT_UPDATE_AVAILABLE
+
+fn EXIT_MANIFEST_STALE(): int
+    return 101
+end EXIT_MANIFEST_STALE
+
+fn EXIT_NO_UPDATES(): int
+    return 102
+end EXIT_NO_UPDATES
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/main.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,294 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: main.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Entry point for Coral package manager
+
+******************************************************************************/
+
+import sys.args
+import sys.process
+import sys.env
+import io.file
+import io.dir
+import core.str
+import types
+import exitcodes as ec
+import commands.build as cmd_build
+import commands.install as cmd_install
+import commands.remove as cmd_remove
+import commands.upgrade as cmd_upgrade
+import commands.info as cmd_info
+import commands.list as cmd_list
+import commands.files as cmd_files
+import commands.owner as cmd_owner
+import commands.search as cmd_search
+import commands.depends as cmd_depends
+import commands.sync as cmd_sync
+import commands.outdated as cmd_outdated
+import commands.repo as cmd_repo
+import commands.key as cmd_key
+import commands.db as cmd_db
+import commands.clean as cmd_clean
+import commands.ports as cmd_ports
+import commands.verify as cmd_verify
+import util.color
+
+fn version(): string
+    return "0.4.2"
+end version
+
+proc print_usage()
+    println("coral " + version() + " - Package manager for Zygaena")
+    println("")
+    println("USAGE:")
+    println("    coral <command> [options] [packages...]")
+    println("")
+    println("COMMANDS:")
+    println("  Package Management:")
+    println("    install <pkg>     Install packages")
+    println("    reinstall <pkg>   Reinstall packages (same as install --force)")
+    println("    remove <pkg>      Remove packages")
+    println("    upgrade           Upgrade packages")
+    println("")
+    println("  Query Commands:")
+    println("    info <pkg>        Show package info")
+    println("    list              List installed packages")
+    println("    files <pkg>       List package files")
+    println("    owner <file>      Find file owner")
+    println("    search <term>     Search packages")
+    println("    depends <pkg>     Show dependencies")
+    println("    outdated          Show available updates")
+    println("    verify <pkg>      Verify package integrity")
+    println("")
+    println("  Build Commands:")
+    println("    build <port>      Build package from port")
+    println("    sync              Update ports tree")
+    println("    clean <target>    Remove build artifacts (sources|packages|build|all)")
+    println("")
+    println("  System Commands:")
+    println("    db <cmd>          Manage package database (init|rebuild|status)")
+    println("    ports <cmd>       Port development tools (new|cache)")
+    println("    repo              Manage repositories")
+    println("    key               Manage signing keys")
+    println("")
+    println("OPTIONS:")
+    println("    -h, --help        Show this help")
+    println("    --version         Show version")
+    println("    -y, --yes         Don't ask for confirmation")
+    println("    -f, --force       Force operation")
+    println("    -v, --verbose     Verbose output")
+    println("    -n, --dry-run     Show what would be done")
+    println("    -q, --quiet       Minimal output")
+    println("    --no-color        Disable colored output")
+    println("    --root <path>     Use alternative root directory")
+    println("    --prefix <path>   Installation prefix (default: /usr)")
+    println("")
+    println("EXIT CODES:")
+    println("    0                 Success")
+    println("    1                 General error")
+    println("    2                 Usage/syntax error")
+    println("    10-19             Package errors")
+    println("    20-29             Build errors")
+    println("    30-39             Network errors")
+    println("    50-59             Permission errors")
+    println("    60-69             Dependency errors")
+    println("    100+              Informational (scripting)")
+end print_usage
+
+// Find the index of the command argument, skipping global flags
+fn find_command_index(): int
+    let argc = args.count()
+    mut i = 1
+    while i < argc
+        let arg = args.get(i)
+
+        // Skip flags that take a value
+        if arg == "--root" or arg == "--prefix"
+            i = i + 2  // Skip flag and its value
+            continue
+        end if
+
+        // Skip boolean flags
+        if arg == "-y" or arg == "--yes" or arg == "-f" or arg == "--force" or arg == "-v" or arg == "--verbose" or arg == "-n" or arg == "--dry-run" or arg == "-q" or arg == "--quiet" or arg == "--no-color"
+            i = i + 1
+            continue
+        end if
+
+        // Found the command
+        return i
+    end while
+
+    return -1
+end find_command_index
+
+// Parse global options from command line arguments
+fn parse_global_options(): types.GlobalOptions
+    mut opts = types.new_global_options()
+
+    opts.yes = args.has_flag("y") or args.has_flag("yes")
+    opts.force = args.has_flag("f") or args.has_flag("force")
+    opts.verbose = args.has_flag("v") or args.has_flag("verbose")
+    opts.dry_run = args.has_flag("n") or args.has_flag("dry-run")
+    opts.quiet = args.has_flag("q") or args.has_flag("quiet")
+    // Check for --root option
+    if args.has_flag("root")
+        opts.root = args.get_flag_value("root")
+    end if
+
+    // Check for --prefix option
+    if args.has_flag("prefix")
+        opts.prefix = args.get_flag_value("prefix")
+    end if
+
+    return opts
+end parse_global_options
+
+// Check if we are running as root (UID 0)
+// Uses file test to check write permission on system directory
+fn is_root(): bool
+    // Try to create a temp file in /var/lib/coral - only root can write there
+    let test_file = "/var/lib/coral/.root_check"
+    if file.writeFile(test_file, "1")
+        // Successfully wrote - we have root-level access
+        // File will be overwritten next time, no need to delete
+        return true
+    end if
+    return false
+end is_root
+
+
+// Check if a command is a query command (doesn't require root)
+fn is_query_command(cmd: string): bool
+    // Query commands that don't modify the system
+    if cmd == "info"
+        return true
+    elif cmd == "list"
+        return true
+    elif cmd == "files"
+        return true
+    elif cmd == "owner"
+        return true
+    elif cmd == "search"
+        return true
+    elif cmd == "depends"
+        return true
+    elif cmd == "outdated"
+        return true
+    elif cmd == "verify"
+        return true
+    elif cmd == "build"
+        // Building packages doesn't require root - only writes to build directories
+        return true
+    elif cmd == "--help" or cmd == "-h"
+        return true
+    elif cmd == "--version" or cmd == "version"
+        return true
+    end if
+    return false
+end is_query_command
+
+fn main(): int
+    let argc = args.count()
+
+    if argc < 2
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    // Check for --help and --version before finding command
+    // (these can appear anywhere)
+    if args.has_flag("help") or args.has_flag("h")
+        print_usage()
+        return ec.EXIT_SUCCESS()
+    end if
+
+    if args.has_flag("version")
+        println("coral " + version())
+        return ec.EXIT_SUCCESS()
+    end if
+
+    // Find the actual command, skipping global flags
+    let cmd_index = find_command_index()
+
+    if cmd_index < 0
+        print_usage()
+        return ec.EXIT_USAGE()
+    end if
+
+    let cmd = args.get(cmd_index)
+
+    // Parse global options and set command index
+    mut opts = parse_global_options()
+    opts.cmd_index = cmd_index
+
+    // Check for root privileges if required
+    if not is_query_command(cmd)
+        if not is_root()
+            color.print_error("This command requires root privileges")
+            println("Run 'coral " + cmd + "' as root or with sudo")
+            return ec.EXIT_ROOT_REQUIRED()
+        end if
+    end if
+
+    // Dispatch to command handlers
+    // Commands return exit codes
+    if cmd == "build"
+        return cmd_build.execute(opts)
+    elif cmd == "install"
+        return cmd_install.execute(opts)
+    elif cmd == "reinstall"
+        // Reinstall is install with force flag enabled
+        mut reinstall_opts = opts
+        reinstall_opts.force = true
+        return cmd_install.execute(reinstall_opts)
+    elif cmd == "remove"
+        return cmd_remove.execute(opts)
+    elif cmd == "upgrade"
+        return cmd_upgrade.execute(opts)
+    elif cmd == "info"
+        return cmd_info.execute(opts)
+    elif cmd == "list"
+        return cmd_list.execute(opts)
+    elif cmd == "files"
+        return cmd_files.execute(opts)
+    elif cmd == "owner"
+        return cmd_owner.execute(opts)
+    elif cmd == "search"
+        return cmd_search.execute()
+    elif cmd == "depends"
+        return cmd_depends.execute()
+    elif cmd == "sync"
+        return cmd_sync.execute()
+    elif cmd == "outdated"
+        return cmd_outdated.execute()
+    elif cmd == "repo"
+        return cmd_repo.execute()
+    elif cmd == "key"
+        return cmd_key.execute()
+    elif cmd == "db"
+        return cmd_db.execute(opts)
+    elif cmd == "clean"
+        return cmd_clean.execute()
+    elif cmd == "ports"
+        return cmd_ports.execute(opts)
+    elif cmd == "verify"
+        return cmd_verify.execute(opts)
+    elif cmd == "version"
+        println(version())
+        return ec.EXIT_SUCCESS()
+    else
+        color.print_error("unknown command '" + cmd + "'")
+        println("Run 'coral --help' for usage.")
+        return ec.EXIT_USAGE()
+    end if
+end main
diff -r 000000000000 -r d0a96d73e9e1 src/types.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/types.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,397 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: types.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Shared type definitions for Coral package manager
+
+******************************************************************************/
+
+module types
+
+export
+    // Path getter functions (constants can't be exported across modules)
+    fn get_ports_dir(): string
+    fn get_sources_dir(): string
+    fn get_packages_dir(): string
+    fn get_build_dir(): string
+    fn get_db_dir(): string
+    fn get_index_dir(): string
+    fn get_repos_dir(): string
+    fn get_config_file(): string
+    fn get_groups_dir(): string
+    // Legacy (for compatibility)
+    fn get_work_dir(): string
+    fn get_pkg_dir(): string
+
+    // Types
+    type PackageInfo
+    type Dependencies
+    type Source
+    type SourceInfo
+    type PatchInfo
+    type ConfigInfo
+    type ScriptsInfo
+    type BuildConfig
+    type Port
+    type InstalledPackage
+    type BuildResult
+    type GlobalOptions
+    type PackageGroup
+    type PortResult
+
+    // Factory functions
+    fn new_package_info(): PackageInfo
+    fn new_dependencies(): Dependencies
+    fn new_global_options(): GlobalOptions
+    fn new_source(): Source
+    fn new_source_info(): SourceInfo
+    fn new_patch_info(): PatchInfo
+    fn new_config_info(): ConfigInfo
+    fn new_scripts_info(): ScriptsInfo
+    fn new_build_config(): BuildConfig
+    fn new_port(): Port
+    fn new_port_result(ok: bool, p: Port, err: string): PortResult
+    fn new_installed_package(): InstalledPackage
+    fn new_package_group(): PackageGroup
+end export
+
+// Zygaena/Coral filesystem paths
+// Ports tree and build artifacts
+const PORTS_DIR: string = "/usr/zports"
+const GROUPS_DIR: string = "/usr/zports/groups"
+const SOURCES_DIR: string = "/usr/zports/pkgs/sources"
+const PACKAGES_DIR: string = "/usr/zports/pkgs/packages"
+const BUILD_DIR: string = "/usr/zports/pkgs/build"
+
+// System state (in /var/lib)
+const DB_DIR: string = "/var/lib/coral/installed"
+const INDEX_DIR: string = "/var/lib/coral/db"
+const REPOS_DIR: string = "/var/lib/coral/repos"
+
+// Configuration
+const CONFIG_FILE: string = "/etc/coral/coral.conf"
+
+// Legacy aliases (for compatibility during transition)
+const WORK_DIR: string = "/usr/zports/pkgs/build"
+const PKG_DIR: string = "/usr/zports/pkgs/build"
+
+// Package metadata from package.toml
+type PackageInfo = struct
+    name: string
+    version: string
+    release: int
+    description: string
+    url: string
+    license: string
+    maintainer: string
+    arch: string
+    alternatives: [string]
+    alternatives_count: int
+    conflicts: [string]
+    conflicts_count: int
+    runtime_deps: [string]
+    build_deps: [string]
+    runtime_deps_count: int
+    build_deps_count: int
+end PackageInfo
+
+// Dependencies
+type Dependencies = struct
+    runtime: [string]
+    build: [string]
+    runtime_count: int
+    build_count: int
+end Dependencies
+
+// Individual source file with mirror support
+type Source = struct
+    file: string        // Output filename (what to save as)
+    urls: [string]      // Mirror URLs (tried in order)
+    url_count: int      // Number of URLs
+    checksum: string    // SHA256 checksum (or "SKIP")
+    extract: bool       // Whether to extract archive (default: true)
+end Source
+
+// Collection of source files to download
+type SourceInfo = struct
+    sources: [Source]   // Array of Source structs
+    count: int          // Number of sources
+end SourceInfo
+
+// Patch configuration
+type PatchInfo = struct
+    files: [string]
+    strip: int
+    count: int
+end PatchInfo
+
+// Config file protection
+type ConfigInfo = struct
+    files: [string]
+    count: int
+end ConfigInfo
+
+// Install/remove scripts
+type ScriptsInfo = struct
+    pre_install: string
+    post_install: string
+    pre_remove: string
+    post_remove: string
+end ScriptsInfo
+
+// Build configuration
+type BuildConfig = struct
+    parallel: bool
+    jobs: int
+end BuildConfig
+
+// Complete port definition
+type Port = struct
+    info: PackageInfo
+    deps: Dependencies
+    sources: SourceInfo
+    patches: PatchInfo
+    config: ConfigInfo
+    scripts: ScriptsInfo
+    build_cfg: BuildConfig
+    port_dir: string
+    category: string
+    // Per-package path overrides (from package.toml [paths] section)
+    prefix: string
+    sysconfdir: string
+end Port
+
+// Installed package record
+type InstalledPackage = struct
+    info: PackageInfo
+    install_date: string
+    install_reason: string
+    files: [string]
+    files_count: int
+end InstalledPackage
+
+// Build result
+type BuildResult = struct
+    success: bool
+    package_path: string
+    error_message: string
+end BuildResult
+
+// Global CLI options
+type GlobalOptions = struct
+    yes: bool
+    force: bool
+    verbose: bool
+    dry_run: bool
+    quiet: bool
+    root: string
+    prefix: string
+    cmd_index: int  // Index of the command in args (for subcommand parsing)
+end GlobalOptions
+
+// Package group (meta-package)
+type PackageGroup = struct
+    name: string
+    description: string
+    members: [string]
+    members_count: int
+end PackageGroup
+
+// Parse result wrapper
+type PortResult = struct
+    success: bool
+    port: Port
+    error: string
+end PortResult
+
+// Initialize empty PackageInfo
+fn new_package_info(): PackageInfo
+    return PackageInfo{
+        name: "",
+        version: "",
+        release: 1,
+        description: "",
+        url: "",
+        license: "",
+        maintainer: "",
+        arch: "x86_64",
+        alternatives: new [string](0),
+        alternatives_count: 0,
+        conflicts: new [string](0),
+        conflicts_count: 0,
+        runtime_deps: new [string](64),
+        build_deps: new [string](64),
+        runtime_deps_count: 0,
+        build_deps_count: 0
+    }
+end new_package_info
+
+// Initialize empty Dependencies
+fn new_dependencies(): Dependencies
+    return Dependencies{
+        runtime: new [string](0),
+        build: new [string](0),
+        runtime_count: 0,
+        build_count: 0
+    }
+end new_dependencies
+
+// Initialize empty GlobalOptions with defaults
+fn new_global_options(): GlobalOptions
+    return GlobalOptions{
+        yes: false,
+        force: false,
+        verbose: false,
+        dry_run: false,
+        quiet: false,
+        root: "",
+        prefix: "",
+        cmd_index: 1
+    }
+end new_global_options
+
+// Initialize empty Source
+fn new_source(): Source
+    return Source{
+        file: "",
+        urls: new [string](16),
+        url_count: 0,
+        checksum: "SKIP",
+        extract: true
+    }
+end new_source
+
+// Initialize empty SourceInfo
+fn new_source_info(): SourceInfo
+    return SourceInfo{
+        sources: new [Source](16),
+        count: 0
+    }
+end new_source_info
+
+// Initialize empty PatchInfo
+fn new_patch_info(): PatchInfo
+    return PatchInfo{
+        files: new [string](0),
+        strip: 1,
+        count: 0
+    }
+end new_patch_info
+
+// Initialize empty ConfigInfo
+fn new_config_info(): ConfigInfo
+    return ConfigInfo{
+        files: new [string](0),
+        count: 0
+    }
+end new_config_info
+
+// Initialize empty ScriptsInfo
+fn new_scripts_info(): ScriptsInfo
+    return ScriptsInfo{
+        pre_install: "",
+        post_install: "",
+        pre_remove: "",
+        post_remove: ""
+    }
+end new_scripts_info
+
+// Initialize default BuildConfig
+fn new_build_config(): BuildConfig
+    return BuildConfig{
+        parallel: true,
+        jobs: 0
+    }
+end new_build_config
+
+// Initialize empty Port
+fn new_port(): Port
+    return Port{
+        info: new_package_info(),
+        deps: new_dependencies(),
+        sources: new_source_info(),
+        patches: new_patch_info(),
+        config: new_config_info(),
+        scripts: new_scripts_info(),
+        build_cfg: new_build_config(),
+        port_dir: "",
+        category: "",
+        prefix: "",
+        sysconfdir: ""
+    }
+end new_port
+
+// Create a PortResult
+fn new_port_result(ok: bool, p: Port, err: string): PortResult
+    return PortResult{
+        success: ok,
+        port: p,
+        error: err
+    }
+end new_port_result
+
+// Path getter functions
+fn get_ports_dir(): string
+    return PORTS_DIR
+end get_ports_dir
+
+fn get_sources_dir(): string
+    return SOURCES_DIR
+end get_sources_dir
+
+fn get_packages_dir(): string
+    return PACKAGES_DIR
+end get_packages_dir
+
+fn get_build_dir(): string
+    return BUILD_DIR
+end get_build_dir
+
+fn get_db_dir(): string
+    return DB_DIR
+end get_db_dir
+
+fn get_index_dir(): string
+    return INDEX_DIR
+end get_index_dir
+
+fn get_repos_dir(): string
+    return REPOS_DIR
+end get_repos_dir
+
+fn get_config_file(): string
+    return CONFIG_FILE
+end get_config_file
+
+fn get_groups_dir(): string
+    return GROUPS_DIR
+end get_groups_dir
+
+// Legacy aliases (for compatibility during transition)
+fn get_work_dir(): string
+    return BUILD_DIR
+end get_work_dir
+
+fn get_pkg_dir(): string
+    return BUILD_DIR
+end get_pkg_dir
+
+// Initialize empty InstalledPackage
+fn new_installed_package(): InstalledPackage
+    return InstalledPackage{ info: new_package_info(), install_date: "", install_reason: "", files: new [string](0), files_count: 0 }
+end new_installed_package
+
+fn new_package_group(): PackageGroup
+    return PackageGroup{ name: "", description: "", members: new [string](256), members_count: 0 }
+end new_package_group
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/util/archive.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/util/archive.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,295 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: archive.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Tar/xz archive operations using shell commands
+
+******************************************************************************/
+
+module util.archive
+
+import sys.process
+import io.file
+import io.dir
+import io.path
+import core.str
+
+export
+    // Archive type enumeration (as strings for simplicity)
+    const ARCHIVE_TAR_GZ: string
+    const ARCHIVE_TAR_XZ: string
+    const ARCHIVE_TAR_BZ2: string
+    const ARCHIVE_ZIP: string
+    const ARCHIVE_UNKNOWN: string
+
+    // Detect archive type from filename extension
+    fn detect_type(archive_path: string): string
+
+    // Extract an archive to destination directory
+    fn extract(archive_path: string, dest_dir: string): bool
+
+    // Create a .pkg.tar.xz package from a directory
+    fn create_package(source_dir: string, output_path: string): bool
+
+    // List contents of an archive (returns count, fills files array)
+    fn list_contents(archive_path: string, files: [string], max_count: int): int
+end export
+
+// Archive type constants
+const ARCHIVE_TAR_GZ: string = "tar.gz"
+const ARCHIVE_TAR_XZ: string = "tar.xz"
+const ARCHIVE_TAR_BZ2: string = "tar.bz2"
+const ARCHIVE_ZIP: string = "zip"
+const ARCHIVE_UNKNOWN: string = "unknown"
+
+// Get the tar command (use gtar on illumos/OmniOS, tar on Linux)
+fn get_tar_cmd(): string
+    // On illumos, we need GNU tar for full compatibility
+    // Check if gtar exists, otherwise fall back to tar (for Linux)
+    if file.fileExists("/usr/bin/gtar")
+        return "/usr/bin/gtar"
+    elif file.fileExists("/usr/gnu/bin/tar")
+        return "/usr/gnu/bin/tar"
+    end if
+    // Fall back to system tar (Linux)
+    return "tar"
+end get_tar_cmd
+
+// Detect archive type from filename
+fn detect_type(archive_path: string): string
+    let lower = str.to_lower(archive_path)
+
+    if str.ends_with(lower, ".tar.xz") or str.ends_with(lower, ".txz")
+        return ARCHIVE_TAR_XZ
+    elif str.ends_with(lower, ".tar.gz") or str.ends_with(lower, ".tgz")
+        return ARCHIVE_TAR_GZ
+    elif str.ends_with(lower, ".tar.bz2") or str.ends_with(lower, ".tbz2") or str.ends_with(lower, ".tbz")
+        return ARCHIVE_TAR_BZ2
+    elif str.ends_with(lower, ".zip")
+        return ARCHIVE_ZIP
+    elif str.ends_with(lower, ".tar")
+        // Plain tar (no compression)
+        return "tar"
+    end if
+
+    return ARCHIVE_UNKNOWN
+end detect_type
+
+// Build the tar extraction flags based on archive type
+fn get_extract_flags(archive_type: string): string
+    if archive_type == ARCHIVE_TAR_XZ
+        return "-Jxf"
+    elif archive_type == ARCHIVE_TAR_GZ
+        return "-zxf"
+    elif archive_type == ARCHIVE_TAR_BZ2
+        return "-jxf"
+    elif archive_type == "tar"
+        return "-xf"
+    end if
+    return ""
+end get_extract_flags
+
+// Build the tar list flags based on archive type
+fn get_list_flags(archive_type: string): string
+    if archive_type == ARCHIVE_TAR_XZ
+        return "-Jtf"
+    elif archive_type == ARCHIVE_TAR_GZ
+        return "-ztf"
+    elif archive_type == ARCHIVE_TAR_BZ2
+        return "-jtf"
+    elif archive_type == "tar"
+        return "-tf"
+    end if
+    return ""
+end get_list_flags
+
+// Extract an archive to the destination directory
+fn extract(archive_path: string, dest_dir: string): bool
+    // Verify archive exists
+    if not file.fileExists(archive_path)
+        return false
+    end if
+
+    // Ensure destination directory exists
+    if not dir.dir_exists(dest_dir)
+        if not dir.create_dir_all(dest_dir)
+            return false
+        end if
+    end if
+
+    let archive_type = detect_type(archive_path)
+
+    if archive_type == ARCHIVE_ZIP
+        // Use unzip for zip files
+        let cmd = "unzip -q -o \"" + archive_path + "\" -d \"" + dest_dir + "\""
+        let pid = process.process_spawn_shell(cmd)
+        if pid < 0
+            return false
+        end if
+        let exit_code = process.process_wait(pid)
+        return exit_code == 0
+    end if
+
+    let flags = get_extract_flags(archive_type)
+    if str.length(flags) == 0
+        return false
+    end if
+
+    // Build tar command
+    let tar = get_tar_cmd()
+    let cmd = tar + " " + flags + " \"" + archive_path + "\" -C \"" + dest_dir + "\""
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return false
+    end if
+
+    let exit_code = process.process_wait(pid)
+    return exit_code == 0
+end extract
+
+// Create a .pkg.tar.xz package from a source directory
+fn create_package(source_dir: string, output_path: string): bool
+    // Verify source directory exists
+    if not dir.dir_exists(source_dir)
+        return false
+    end if
+
+    // Ensure parent directory of output exists
+    let output_dir = path.dirname(output_path)
+    if str.length(output_dir) > 0 and not dir.dir_exists(output_dir)
+        if not dir.create_dir_all(output_dir)
+            return false
+        end if
+    end if
+
+    // Create tar.xz archive
+    // Command: cd source_dir && tar -Jcf output_path .
+    let tar = get_tar_cmd()
+    let cmd = "cd \"" + source_dir + "\" && " + tar + " -Jcf \"" + output_path + "\" ."
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return false
+    end if
+
+    let exit_code = process.process_wait(pid)
+    return exit_code == 0
+end create_package
+
+// List contents of an archive
+fn list_contents(archive_path: string, files: [string], max_count: int): int
+    if not file.fileExists(archive_path)
+        return 0
+    end if
+
+    let archive_type = detect_type(archive_path)
+
+    if archive_type == ARCHIVE_ZIP
+        // For zip files, use unzip -l and parse output
+        // This is more complex, so for now return 0
+        // TODO: Implement zip listing if needed
+        return 0
+    end if
+
+    let flags = get_list_flags(archive_type)
+    if str.length(flags) == 0
+        return 0
+    end if
+
+    // Build tar command with output to temp file
+    let tar = get_tar_cmd()
+    let tmp_file = "/tmp/coral_archive_list_" + int_to_string(process.getpid()) + ".txt"
+    let cmd = tar + " " + flags + " \"" + archive_path + "\" > \"" + tmp_file + "\" 2>/dev/null"
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return 0
+    end if
+
+    let exit_code = process.process_wait(pid)
+    if exit_code != 0
+        return 0
+    end if
+
+    // Read the temp file
+    if not file.fileExists(tmp_file)
+        return 0
+    end if
+
+    let content = file.readFile(tmp_file)
+
+    // Clean up temp file
+    cleanup_temp_file(tmp_file)
+
+    if str.length(content) == 0
+        return 0
+    end if
+
+    // Split by newlines
+    mut lines: [string] = new [string](max_count)
+    let line_count = str.split(content, '\n', lines, max_count)
+
+    // Copy non-empty lines to output
+    mut count = 0
+    mut i = 0
+    while i < line_count and count < max_count
+        let line = str.trim_ws(lines[i])
+        if str.length(line) > 0
+            files[count] = line
+            count = count + 1
+        end if
+        i = i + 1
+    end while
+
+    return count
+end list_contents
+
+// Helper: remove temp file
+fn cleanup_temp_file(path: string): bool
+    let cmd = "rm -f \"" + path + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return false
+    end if
+    process.process_wait(pid)
+    return true
+end cleanup_temp_file
+
+// Helper: convert int to string (local copy to avoid circular import)
+fn int_to_string(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_string
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/util/checksum.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/util/checksum.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,138 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: checksum.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: SHA256 checksum verification utilities
+
+******************************************************************************/
+
+module util.checksum
+
+import io.file
+import core.str
+import sys.process
+import crypto.sha256 as sha
+
+export
+    // Compute SHA256 hash of a file, returns hex string (lowercase)
+    fn sha256_file(path: string): string
+
+    // Compute SHA256 hash of a string, returns hex string (lowercase)
+    fn sha256_string(data: string): string
+
+    // Verify a file matches expected checksum
+    fn verify_checksum(path: string, expected: string): bool
+
+    // Parse checksum format "algo:hash" and verify
+    fn verify_checksum_with_algo(path: string, checksum_str: string): bool
+end export
+
+// Compute SHA256 of a file
+// Uses Reef's native streaming sha256_file() (Reef 0.1.14+)
+// Falls back to sha256sum command if native fails
+fn sha256_file(path: string): string
+    if not file.fileExists(path)
+        return ""
+    end if
+
+    // Use native streaming API (memory efficient, works on all file sizes)
+    // Reef 0.1.14 fixed the large file hang/segfault issue
+    let hash = sha.sha256_file(path)
+    if str.length(hash) == 64
+        return str.to_lower(hash)
+    end if
+
+    // Fallback to sha256sum command if native fails
+    return sha256_file_cmd(path)
+end sha256_file
+
+// Compute SHA256 using sha256sum command (for large files)
+fn sha256_file_cmd(path: string): string
+    let output_file = "/tmp/coral_checksum_" + extract_basename(path)
+    let cmd = "sha256sum \"" + path + "\" | cut -d' ' -f1 > \"" + output_file + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        return ""
+    end if
+    process.process_wait(pid)
+
+    // Read the hash from output file
+    if not file.fileExists(output_file)
+        return ""
+    end if
+
+    let hash = str.trim_ws(file.readFile(output_file))
+
+    // Clean up
+    let rm_pid = process.process_spawn_shell("rm -f \"" + output_file + "\"")
+    if rm_pid > 0
+        process.process_wait(rm_pid)
+    end if
+
+    return hash
+end sha256_file_cmd
+
+// Extract basename from path for temp file naming
+fn extract_basename(path: string): string
+    let last_slash = str.last_index_of_char(path, '/')
+    if last_slash < 0
+        return path
+    end if
+    return str.substring(path, last_slash + 1, str.length(path) - last_slash - 1)
+end extract_basename
+
+// Compute SHA256 of a string using native crypto
+fn sha256_string(data: string): string
+    let hash = sha.sha256(data)
+    return str.to_lower(hash)
+end sha256_string
+
+// Verify that a file's SHA256 matches the expected value
+fn verify_checksum(path: string, expected: string): bool
+    let computed = sha256_file(path)
+    if str.length(computed) == 0
+        return false
+    end if
+
+    // Compare case-insensitively (normalize to lowercase)
+    let computed_lower = str.to_lower(computed)
+    let expected_lower = str.to_lower(expected)
+
+    return computed_lower == expected_lower
+end verify_checksum
+
+// Parse and verify checksum in format "algo:hash" or just "hash"
+// Supported algorithms: sha256, sha256sum
+// If no algorithm prefix, assumes sha256
+fn verify_checksum_with_algo(path: string, checksum_str: string): bool
+    // Check for algorithm prefix
+    let colon_idx = str.index_of_char(checksum_str, ':')
+
+    if colon_idx < 0
+        // No prefix, assume SHA256
+        return verify_checksum(path, checksum_str)
+    end if
+
+    // Extract algorithm and hash
+    let algo = str.to_lower(str.substring(checksum_str, 0, colon_idx))
+    let hash = str.substring(checksum_str, colon_idx + 1, str.length(checksum_str) - colon_idx - 1)
+
+    // Verify based on algorithm
+    if algo == "sha256" or algo == "sha256sum"
+        return verify_checksum(path, hash)
+    end if
+
+    // Unsupported algorithm
+    return false
+end verify_checksum_with_algo
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/util/color.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/util/color.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,170 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: color.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Terminal color utilities for Coral
+
+******************************************************************************/
+
+module util.color
+
+import sys.args
+import sys.env
+import sys.platform.runtime as runtime
+
+export
+    // Color functions - wrap text in ANSI color codes
+    fn bold(s: string): string
+    fn dim(s: string): string
+    fn red(s: string): string
+    fn green(s: string): string
+    fn yellow(s: string): string
+    fn blue(s: string): string
+    fn cyan(s: string): string
+    fn magenta(s: string): string
+
+    // Formatted output helpers
+    proc print_error(msg: string)
+    proc print_success(msg: string)
+    proc print_warning(msg: string)
+    proc print_info(msg: string)
+    proc print_action(msg: string)
+end export
+
+// ANSI escape code prefix: ESC[ (ASCII 27)
+// Reset code: ESC[0m
+
+// Helper to convert int to char (for ESC character)
+fn int_to_char(n: int): char
+    unsafe
+        let result: char = n
+        return result
+    end unsafe
+end int_to_char
+
+// Get ESC character (ASCII 27) as a single-character string
+fn get_esc(): string
+    unsafe
+        let result: string = runtime.reef_alloc_string_buffer(2)
+        result[0] = int_to_char(27)
+        return result
+    end unsafe
+end get_esc
+
+// Check if color output should be disabled
+// Respects --no-color flag and NO_COLOR environment variable
+fn no_color(): bool
+    if args.has_flag("no-color")
+        return true
+    end if
+    if env.has_env("NO_COLOR")
+        return true
+    end if
+    return false
+end no_color
+
+// Helper to wrap text with ANSI codes
+fn wrap_color(s: string, code: string): string
+    if no_color()
+        return s
+    end if
+    // ESC[ + code + m + text + ESC[0m
+    let esc = get_esc()
+    return esc + "[" + code + "m" + s + esc + "[0m"
+end wrap_color
+
+// Bold text (code 1)
+fn bold(s: string): string
+    return wrap_color(s, "1")
+end bold
+
+// Dim text (code 2)
+fn dim(s: string): string
+    return wrap_color(s, "2")
+end dim
+
+// Red text (code 31)
+fn red(s: string): string
+    return wrap_color(s, "31")
+end red
+
+// Green text (code 32)
+fn green(s: string): string
+    return wrap_color(s, "32")
+end green
+
+// Yellow text (code 33)
+fn yellow(s: string): string
+    return wrap_color(s, "33")
+end yellow
+
+// Blue text (code 34)
+fn blue(s: string): string
+    return wrap_color(s, "34")
+end blue
+
+// Magenta text (code 35)
+fn magenta(s: string): string
+    return wrap_color(s, "35")
+end magenta
+
+// Cyan text (code 36)
+fn cyan(s: string): string
+    return wrap_color(s, "36")
+end cyan
+
+// Print error message with red X prefix
+proc print_error(msg: string)
+    if no_color()
+        println("ERROR: " + msg)
+    else
+        println(red("error:") + " " + msg)
+    end if
+end print_error
+
+// Print success message with green checkmark prefix
+proc print_success(msg: string)
+    if no_color()
+        println("OK: " + msg)
+    else
+        println(green("ok:") + " " + msg)
+    end if
+end print_success
+
+// Print warning message with yellow prefix
+proc print_warning(msg: string)
+    if no_color()
+        println("WARNING: " + msg)
+    else
+        println(yellow("warning:") + " " + msg)
+    end if
+end print_warning
+
+// Print info message with blue arrow prefix
+proc print_info(msg: string)
+    if no_color()
+        println("-> " + msg)
+    else
+        println(blue("->") + " " + msg)
+    end if
+end print_info
+
+// Print action message with bold arrow prefix
+proc print_action(msg: string)
+    if no_color()
+        println("=> " + msg)
+    else
+        println(bold("=>") + " " + msg)
+    end if
+end print_action
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/util/http.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/util/http.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,273 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: http.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: HTTP download utilities using native Reef net.http
+             Requires Reef 0.1.16+ for progress bar support
+
+******************************************************************************/
+
+module util.http
+
+import net.http
+import net.extras
+import io.file
+import core.str
+import sys.process
+import util.color
+
+export
+    // Download a file from URL to destination path
+    fn download(url: string, dest: string): bool
+
+    // Download with verbose progress output
+    fn download_verbose(url: string, dest: string): bool
+
+    // Fetch URL content as text
+    fn fetch_text(url: string): string
+
+    // Fetch URL content with timeout
+    fn fetch_text_timeout(url: string, timeout_ms: int): string
+
+    // Check if a URL is reachable (HEAD request)
+    fn check_url(url: string): bool
+
+    // Get response status code for a URL
+    fn get_status(url: string): int
+
+    // Configure proxy usage (call before downloads)
+    proc enable_proxy(use_proxy: bool)
+end export
+
+// Module-level proxy setting
+mut proxy_enabled = false
+
+// Default timeout in milliseconds (30 seconds)
+fn default_timeout(): int
+    return 30000
+end default_timeout
+
+// Internal: download with progress bar (Style C: solid block)
+// Returns true on success, false on failure
+fn download_with_progress(url: string, dest: string): bool
+    // Configure progress bar - Style C: solid block character
+    extras.progress_bar_set_char("█")
+    extras.progress_bar_set_width(30)
+    extras.progress_bar_init(url)
+
+    // Download with progress callback
+    let cb = fn(p: http.HttpDownloadProgress): bool => extras.progress_bar(p)
+    return http.http_download_file_callback(url, dest, cb)
+end download_with_progress
+
+// Check if URL will redirect (3xx status) - native HTTP download crashes on redirects
+fn url_will_redirect(url: string): bool
+    let status = get_status(url)
+    return status >= 300 and status < 400
+end url_will_redirect
+
+// Download a file from URL to destination with progress bar
+// Uses native Reef http_download_file_callback() (requires Reef 0.1.16+)
+// Falls back to curl if native download fails or URL redirects
+fn download(url: string, dest: string): bool
+    let filename = extract_filename(url)
+    print("   " + filename + " ")
+
+    // Check for redirects first - native HTTP crashes on 302s
+    if not url_will_redirect(url)
+        // Try native Reef download with progress bar
+        if download_with_progress(url, dest)
+            println("")  // Newline after progress bar completes
+            return true
+        end if
+        println("")  // Newline after failed progress
+    end if
+
+    // Fallback to curl (handles redirects with -L flag)
+    print("   (using curl) ... ")
+    if download_with_curl(url, dest)
+        println("done")
+        return true
+    end if
+
+    println("failed")
+    return false
+end download
+
+// Download with verbose progress output
+// Uses native Reef http_download_file_callback() with progress bar (requires Reef 0.1.16+)
+fn download_verbose(url: string, dest: string): bool
+    let filename = extract_filename(url)
+    println("   " + url)
+    print("   " + filename + " ")
+
+    // Check for redirects first - native HTTP crashes on 302s
+    if not url_will_redirect(url)
+        // Try native Reef download with progress bar
+        if download_with_progress(url, dest)
+            println("")  // Newline after progress bar completes
+            color.print_success("Download complete: " + filename)
+            return true
+        end if
+        println("")  // Newline after failed progress
+    end if
+
+    // Fallback to curl (handles redirects with -L flag)
+    print("   (using curl) ... ")
+    if download_with_curl(url, dest)
+        println("done")
+        color.print_success("Download complete: " + filename)
+        return true
+    end if
+
+    println("failed")
+    color.print_error("Download failed: " + url)
+    return false
+end download_verbose
+
+// Fetch URL content as text
+fn fetch_text(url: string): string
+    return fetch_text_timeout(url, default_timeout())
+end fetch_text
+
+// Fetch URL content with timeout
+// Uses http_get_auto() which handles redirects and is safe for text/binary
+fn fetch_text_timeout(url: string, timeout_ms: int): string
+    // Create request with timeout
+    let req = http.http_request_new("GET", url)
+    http.http_request_set_header(req, "User-Agent", "Coral/1.0")
+
+    let resp = http.http_send_timeout(req, timeout_ms)
+
+    // Check for errors
+    if str.length(resp.error) > 0
+        return ""
+    end if
+
+    // Check for success
+    if not http.http_response_is_ok(resp)
+        return ""
+    end if
+
+    return http.http_response_body(resp)
+end fetch_text_timeout
+
+// Check if a URL is reachable
+fn check_url(url: string): bool
+    let req = http.http_request_new("HEAD", url)
+    http.http_request_set_header(req, "User-Agent", "Coral/1.0")
+
+    let resp = http.http_send_timeout(req, 10000)
+
+    if str.length(resp.error) > 0
+        return false
+    end if
+
+    // Accept success codes and redirects as "reachable"
+    return resp.status_code >= 200 and resp.status_code < 400
+end check_url
+
+// Get response status code for a URL
+fn get_status(url: string): int
+    let req = http.http_request_new("HEAD", url)
+    http.http_request_set_header(req, "User-Agent", "Coral/1.0")
+
+    let resp = http.http_send_timeout(req, 10000)
+
+    if str.length(resp.error) > 0
+        return 0
+    end if
+
+    return resp.status_code
+end get_status
+
+// Extract filename from URL
+fn extract_filename(url: string): string
+    // Find last slash
+    let last_slash = str.last_index_of_char(url, '/')
+    if last_slash < 0
+        return url
+    end if
+
+    let filename = str.substring(url, last_slash + 1, str.length(url) - last_slash - 1)
+
+    // Remove query string if present
+    let query_pos = str.index_of_char(filename, '?')
+    if query_pos > 0
+        return str.substring(filename, 0, query_pos)
+    end if
+
+    return filename
+end extract_filename
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+proc enable_proxy(use_proxy: bool)
+    proxy_enabled = use_proxy
+end enable_proxy
+
+// Download using curl command
+// Handles HTTP redirects, chunked encoding, and SSL/TLS properly
+fn download_with_curl(url: string, dest: string): bool
+    // Use curl with:
+    // -L: follow redirects (required for GitHub, etc.)
+    // -f: fail silently on HTTP errors (returns non-zero exit code)
+    // -s: silent mode (no progress meter)
+    // -S: show errors even in silent mode
+    // -o: output file
+    mut proxy_flag = " --noproxy \"*\""
+    if proxy_enabled
+        proxy_flag = ""
+    end if
+    let cmd = "curl -L -f -s -S" + proxy_flag + " -o \"" + dest + "\" \"" + url + "\""
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid < 0
+        color.print_error("Failed to spawn curl")
+        return false
+    end if
+
+    let exit_code = process.process_wait(pid)
+    if exit_code != 0
+        color.print_error("Download failed: " + url)
+        return false
+    end if
+
+    return true
+end download_with_curl
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/util/mtree.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/util/mtree.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,609 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: mtree.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: mtree-format manifest for package file inventory
+
+******************************************************************************/
+
+module util.mtree
+
+import io.file
+import io.dir
+import io.path
+import core.str
+import fs.stat
+import fs.link
+import crypto.sha256 as sha
+import util.checksum
+import text.stringbuilder as sb
+
+export
+    // Manifest entry representing a single file, directory, or symlink
+    type ManifestEntry
+
+    // Parse an mtree manifest string into an array of entries
+    // Returns the number of entries parsed
+    fn parse_manifest(content: string, entries: [ManifestEntry], max_entries: int): int
+
+    // Generate an mtree manifest string from a package staging directory
+    fn generate_manifest(pkg_dir: string): string
+
+    // Accessors
+    fn entry_path(entry: ManifestEntry): string
+    fn entry_type(entry: ManifestEntry): string
+    fn entry_mode(entry: ManifestEntry): int
+    fn entry_uname(entry: ManifestEntry): string
+    fn entry_gname(entry: ManifestEntry): string
+    fn entry_sha256(entry: ManifestEntry): string
+    fn entry_link(entry: ManifestEntry): string
+
+    // Format a ManifestEntry as an mtree line
+    fn format_entry(entry: ManifestEntry): string
+
+    // Uid/gid name resolution helpers
+    fn uid_to_name(uid: int): string
+    fn gid_to_name(gid: int): string
+    fn name_to_uid(name: string): int
+    fn name_to_gid(name: string): int
+end export
+
+// ============================================================================
+// Types
+// ============================================================================
+
+type ManifestEntry = struct
+    epath: string       // Relative path (e.g., ./usr/bin/foo)
+    etype: string       // "file", "dir", or "link"
+    mode: int           // Permission bits as integer (e.g., 0755 = 493)
+    uname: string       // Owner user name
+    gname: string       // Owner group name
+    sha256: string      // SHA256 checksum (files only)
+    link_target: string // Symlink target (links only)
+end ManifestEntry
+
+// ============================================================================
+// Accessors
+// ============================================================================
+
+fn entry_path(entry: ManifestEntry): string
+    return entry.epath
+end entry_path
+
+fn entry_type(entry: ManifestEntry): string
+    return entry.etype
+end entry_type
+
+fn entry_mode(entry: ManifestEntry): int
+    return entry.mode
+end entry_mode
+
+fn entry_uname(entry: ManifestEntry): string
+    return entry.uname
+end entry_uname
+
+fn entry_gname(entry: ManifestEntry): string
+    return entry.gname
+end entry_gname
+
+fn entry_sha256(entry: ManifestEntry): string
+    return entry.sha256
+end entry_sha256
+
+fn entry_link(entry: ManifestEntry): string
+    return entry.link_target
+end entry_link
+
+// ============================================================================
+// Parser
+// ============================================================================
+
+// Parse an mtree manifest string into entries.
+// Format per line: ./path type=T mode=OOOO uname=U gname=G [sha256=H] [link=T]
+// Lines starting with # are comments. Empty lines are skipped.
+fn parse_manifest(content: string, entries: [ManifestEntry], max_entries: int): int
+    if str.length(content) == 0
+        return 0
+    end if
+
+    // Split into lines
+    mut lines: [string] = new [string](max_entries + 64)
+    let line_count = str.split(content, '\n', lines, max_entries + 64)
+
+    mut count = 0
+    mut i = 0
+    while i < line_count and count < max_entries
+        let line = str.trim_ws(lines[i])
+        let line_len = str.length(line)
+
+        // Skip empty lines and comments
+        if line_len == 0
+            i = i + 1
+            continue
+        end if
+        if line[0] == '#'
+            i = i + 1
+            continue
+        end if
+
+        // Parse the line: first token is path, rest are key=value
+        let entry = parse_entry_line(line)
+        if str.length(entry.epath) > 0
+            entries[count] = entry
+            count = count + 1
+        end if
+
+        i = i + 1
+    end while
+
+    return count
+end parse_manifest
+
+// Parse a single mtree line into a ManifestEntry
+fn parse_entry_line(line: string): ManifestEntry
+    mut entry = ManifestEntry{
+        epath: "",
+        etype: "",
+        mode: 0,
+        uname: "root",
+        gname: "root",
+        sha256: "",
+        link_target: ""
+    }
+
+    // Split line by spaces
+    mut tokens: [string] = new [string](16)
+    let token_count = str.split(line, ' ', tokens, 16)
+    if token_count == 0
+        return entry
+    end if
+
+    // First token is always the path
+    entry.epath = tokens[0]
+
+    // Remaining tokens are key=value pairs
+    mut t = 1
+    while t < token_count
+        let token = tokens[t]
+        let eq_pos = str.index_of_char(token, '=')
+        if eq_pos > 0
+            let key = str.substring(token, 0, eq_pos)
+            let val = str.substring(token, eq_pos + 1, str.length(token) - eq_pos - 1)
+
+            if str.equals(key, "type")
+                entry.etype = val
+            elif str.equals(key, "mode")
+                entry.mode = octal_to_int(val)
+            elif str.equals(key, "uname")
+                entry.uname = val
+            elif str.equals(key, "gname")
+                entry.gname = val
+            elif str.equals(key, "sha256")
+                entry.sha256 = val
+            elif str.equals(key, "link")
+                entry.link_target = val
+            end if
+        end if
+        t = t + 1
+    end while
+
+    return entry
+end parse_entry_line
+
+// ============================================================================
+// Generator
+// ============================================================================
+
+// Generate an mtree manifest for all files in a package staging directory.
+// Walks the directory tree, collects entries, sorts them, and returns the
+// formatted manifest string.
+fn generate_manifest(pkg_dir: string): string
+    // Collect entries by walking the directory tree
+    mut entries: [ManifestEntry] = new [ManifestEntry](8192)
+    let count = walk_directory(pkg_dir, pkg_dir, entries, 0, 8192)
+
+    if count == 0
+        return ""
+    end if
+
+    // Sort entries by path (simple insertion sort — fine for package file counts)
+    sort_entries(entries, count)
+
+    // Build the output string using StringBuilder to avoid O(n²) heap
+    // fragmentation from repeated string concatenation (Reef BUG-027)
+    let builder = sb.sb_new()
+    sb.sb_append(builder, "#mtree\n")
+    mut i = 0
+    while i < count
+        sb.sb_append(builder, format_entry(entries[i]))
+        sb.sb_append(builder, "\n")
+        i = i + 1
+    end while
+
+    return sb.sb_build(builder)
+end generate_manifest
+
+// Recursively walk a directory, collecting ManifestEntry records.
+// base_dir is the package root (for computing relative paths).
+// current_dir is the directory being walked.
+// Returns total number of entries added (starting from offset).
+fn walk_directory(base_dir: string, current_dir: string, entries: [ManifestEntry], offset: int, max: int): int
+    mut count = offset
+
+    // Add the current directory itself (unless it's the base)
+    if not str.equals(current_dir, base_dir) and count < max
+        let rel = get_relative_path(base_dir, current_dir)
+        if not is_metadata_path(rel)
+            let mode = stat.file_mode(current_dir)
+            let uid = stat.file_uid(current_dir)
+            let gid = stat.file_gid(current_dir)
+            entries[count] = ManifestEntry{
+                epath: "./" + rel,
+                etype: "dir",
+                mode: mode,
+                uname: uid_to_name(uid),
+                gname: gid_to_name(gid),
+                sha256: "",
+                link_target: ""
+            }
+            count = count + 1
+        end if
+    end if
+
+    // List directory contents
+    mut dir_entries: [string] = new [string](4096)
+    let entry_count = dir.list_dir(current_dir, dir_entries, 4096)
+
+    mut i = 0
+    while i < entry_count and count < max
+        let name = dir_entries[i]
+
+        // Skip . and .. to prevent infinite recursion
+        if str.equals(name, ".") or str.equals(name, "..")
+            i = i + 1
+            continue
+        end if
+
+        let full_path = path.join_path(current_dir, name)
+        let rel = get_relative_path(base_dir, full_path)
+
+        // Skip metadata files and directories
+        if is_metadata_path(rel)
+            i = i + 1
+            continue
+        end if
+
+        // Check type: symlink first (symlinks to dirs would match is_directory)
+        if stat.is_symlink(full_path)
+            let target = link.readlink(full_path)
+            let uid = stat.file_uid(current_dir)
+            let gid = stat.file_gid(current_dir)
+            entries[count] = ManifestEntry{
+                epath: "./" + rel,
+                etype: "link",
+                mode: 0,
+                uname: uid_to_name(uid),
+                gname: gid_to_name(gid),
+                sha256: "",
+                link_target: target
+            }
+            count = count + 1
+        elif stat.is_directory(full_path)
+            // Recurse into subdirectory
+            count = walk_directory(base_dir, full_path, entries, count, max)
+        elif stat.is_file(full_path)
+            let mode = stat.file_mode(full_path)
+            let uid = stat.file_uid(full_path)
+            let gid = stat.file_gid(full_path)
+            let hash = checksum.sha256_file(full_path)
+            entries[count] = ManifestEntry{
+                epath: "./" + rel,
+                etype: "file",
+                mode: mode,
+                uname: uid_to_name(uid),
+                gname: gid_to_name(gid),
+                sha256: hash,
+                link_target: ""
+            }
+            count = count + 1
+        end if
+
+        i = i + 1
+    end while
+
+    return count
+end walk_directory
+
+// Check if a relative path is a package metadata file/directory
+fn is_metadata_path(rel: string): bool
+    if str.equals(rel, ".PKGINFO") or str.equals(rel, ".MANIFEST")
+        return true
+    end if
+    if str.equals(rel, ".FOOTPRINT") or str.equals(rel, ".CONFIG")
+        return true
+    end if
+    if str.equals(rel, ".SCRIPTS")
+        return true
+    end if
+    if str.starts_with(rel, ".SCRIPTS/")
+        return true
+    end if
+    return false
+end is_metadata_path
+
+// Get relative path by stripping the base directory prefix
+fn get_relative_path(base: string, full: string): string
+    let base_len = str.length(base)
+    let full_len = str.length(full)
+
+    if full_len <= base_len
+        return ""
+    end if
+
+    // Skip the base prefix and the trailing /
+    mut start = base_len
+    if start < full_len and full[start] == '/'
+        start = start + 1
+    end if
+
+    return str.substring(full, start, full_len - start)
+end get_relative_path
+
+// ============================================================================
+// Formatting
+// ============================================================================
+
+// Format a ManifestEntry as a single mtree line
+fn format_entry(entry: ManifestEntry): string
+    mut line = entry.epath + " type=" + entry.etype
+
+    if str.equals(entry.etype, "file") or str.equals(entry.etype, "dir")
+        line = line + " mode=" + int_to_octal(entry.mode)
+    end if
+
+    line = line + " uname=" + entry.uname
+    line = line + " gname=" + entry.gname
+
+    if str.equals(entry.etype, "file") and str.length(entry.sha256) > 0
+        line = line + " sha256=" + entry.sha256
+    end if
+
+    if str.equals(entry.etype, "link") and str.length(entry.link_target) > 0
+        line = line + " link=" + entry.link_target
+    end if
+
+    return line
+end format_entry
+
+// ============================================================================
+// Sorting
+// ============================================================================
+
+// Insertion sort entries by path (lexicographic).
+// Package file counts are typically <1000, so insertion sort is adequate.
+proc sort_entries(entries: [ManifestEntry], count: int)
+    mut i = 1
+    while i < count
+        let key_entry = entries[i]
+        let key_path = entries[i].epath
+        mut j = i - 1
+        while j >= 0 and str.compare(entries[j].epath, key_path) > 0
+            entries[j + 1] = entries[j]
+            j = j - 1
+        end while
+        entries[j + 1] = key_entry
+        i = i + 1
+    end while
+end sort_entries
+
+// ============================================================================
+// Numeric Conversion Helpers
+// ============================================================================
+
+// Convert an octal string (e.g., "0755", "4755") to an integer
+fn octal_to_int(s: string): int
+    let slen = str.length(s)
+    if slen == 0
+        return 0
+    end if
+
+    mut result = 0
+    mut i = 0
+    while i < slen
+        let c = s[i]
+        if c >= '0' and c <= '7'
+            result = result * 8 + (c - '0')
+        end if
+        i = i + 1
+    end while
+
+    return result
+end octal_to_int
+
+// Convert an integer to a 4-digit octal string (e.g., 493 -> "0755")
+fn int_to_octal(n: int): string
+    if n == 0
+        return "0000"
+    end if
+
+    mut value = n
+    mut digits: [int] = new [int](16)
+    mut count = 0
+
+    while value > 0 and count < 16
+        digits[count] = value - (value / 8 * 8)
+        value = value / 8
+        count = count + 1
+    end while
+
+    // Pad to 4 digits minimum
+    while count < 4
+        digits[count] = 0
+        count = count + 1
+    end while
+
+    // Build string in reverse (most significant first)
+    mut result = ""
+    mut i = count - 1
+    while i >= 0
+        result = result + digit_char(digits[i])
+        i = i - 1
+    end while
+
+    return result
+end int_to_octal
+
+// Single digit to string character
+fn digit_char(d: int): string
+    if d == 0
+        return "0"
+    elif d == 1
+        return "1"
+    elif d == 2
+        return "2"
+    elif d == 3
+        return "3"
+    elif d == 4
+        return "4"
+    elif d == 5
+        return "5"
+    elif d == 6
+        return "6"
+    elif d == 7
+        return "7"
+    end if
+    return "0"
+end digit_char
+
+// Convert int to decimal string (local helper to avoid circular imports)
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value - (value / 10 * 10)
+        result = str.concat(digit_char(digit), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+// ============================================================================
+// UID/GID Name Resolution
+// ============================================================================
+
+// Resolve a numeric uid to a username.
+// Uses POSIX getpwuid() via Reef's fs.stat.uid_name() — zero heap allocation.
+fn uid_to_name(uid: int): string
+    return stat.uid_name(uid)
+end uid_to_name
+
+// Resolve a numeric gid to a group name.
+// Uses POSIX getgrgid() via Reef's fs.stat.gid_name() — zero heap allocation.
+fn gid_to_name(gid: int): string
+    return stat.gid_name(gid)
+end gid_to_name
+
+// Resolve a username to numeric uid by reading /etc/passwd.
+// Only used during verify/install (low call count), not the hot manifest walk.
+fn name_to_uid(name: string): int
+    if str.equals(name, "root")
+        return 0
+    end if
+
+    let content = file.readFile("/etc/passwd")
+    if str.length(content) == 0
+        return 0
+    end if
+
+    mut lines: [string] = new [string](256)
+    let line_count = str.split(content, '\n', lines, 256)
+
+    mut i = 0
+    while i < line_count
+        let line = lines[i]
+        if str.length(line) > 0
+            mut fields: [string] = new [string](8)
+            let field_count = str.split(line, ':', fields, 8)
+            if field_count >= 3 and str.equals(fields[0], name)
+                return str_to_int(fields[2])
+            end if
+        end if
+        i = i + 1
+    end while
+    return 0
+end name_to_uid
+
+// Resolve a group name to numeric gid by reading /etc/group.
+// Only used during verify/install (low call count), not the hot manifest walk.
+fn name_to_gid(name: string): int
+    if str.equals(name, "root")
+        return 0
+    end if
+
+    let content = file.readFile("/etc/group")
+    if str.length(content) == 0
+        return 0
+    end if
+
+    mut lines: [string] = new [string](256)
+    let line_count = str.split(content, '\n', lines, 256)
+
+    mut i = 0
+    while i < line_count
+        let line = lines[i]
+        if str.length(line) > 0
+            mut fields: [string] = new [string](8)
+            let field_count = str.split(line, ':', fields, 8)
+            if field_count >= 3 and str.equals(fields[0], name)
+                return str_to_int(fields[2])
+            end if
+        end if
+        i = i + 1
+    end while
+    return 0
+end name_to_gid
+
+// Simple string-to-int conversion
+fn str_to_int(s: string): int
+    let slen = str.length(s)
+    if slen == 0
+        return 0
+    end if
+
+    mut result = 0
+    mut i = 0
+    while i < slen
+        let c = s[i]
+        if c >= '0' and c <= '9'
+            result = result * 10 + (c - '0')
+        end if
+        i = i + 1
+    end while
+    return result
+end str_to_int
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/util/prompt.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/util/prompt.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,186 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: prompt.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: User prompt utilities for Coral
+
+******************************************************************************/
+
+module util.prompt
+
+import io.console
+import core.str
+import core.convert
+import util.color
+import types
+
+export
+    fn has_yes_flag(opts: types.GlobalOptions): bool
+    fn confirm(message: string, opts: types.GlobalOptions): bool
+    fn confirm_packages(action: string, packages: [string], count: int, opts: types.GlobalOptions): bool
+    fn confirm_install(to_install: [string], install_count: int, to_upgrade: [string], upgrade_count: int, opts: types.GlobalOptions): bool
+    fn confirm_remove(to_remove: [string], count: int, dependents: [string], dep_count: int, opts: types.GlobalOptions): bool
+    fn confirm_build_plan(packages: [string], versions: [string], count: int, target: string, opts: types.GlobalOptions): bool
+end export
+
+// Check if -y or --yes flag is present
+fn has_yes_flag(opts: types.GlobalOptions): bool
+    return opts.yes
+end has_yes_flag
+
+// Ask for confirmation, returns true if user confirms
+fn confirm(message: string, opts: types.GlobalOptions): bool
+    // Skip if -y flag
+    if has_yes_flag(opts)
+        return true
+    end if
+
+    print(message + " [Y/n] ")
+    let response = console.readLine()
+    let r = str.trim_ws(response)
+
+    // Empty or Y/y means yes
+    if str.length(r) == 0
+        return true
+    end if
+    if r == "Y" or r == "y" or r == "yes" or r == "Yes" or r == "YES"
+        return true
+    end if
+
+    return false
+end confirm
+
+// Display list of packages and ask for confirmation
+fn confirm_packages(action: string, packages: [string], count: int, opts: types.GlobalOptions): bool
+    if count == 0
+        return false
+    end if
+
+    println("")
+    println(color.bold("Packages to " + action + ":"))
+    println("")
+
+    // Display packages in columns
+    mut i = 0
+    while i < count
+        println("  " + packages[i])
+        i = i + 1
+    end while
+
+    println("")
+    println(color.dim("Total: " + convert.to_string(count) + " package(s)"))
+    println("")
+
+    return confirm("Proceed?", opts)
+end confirm_packages
+
+// Display package info and ask for install confirmation
+fn confirm_install(to_install: [string], install_count: int,
+                   to_upgrade: [string], upgrade_count: int,
+                   opts: types.GlobalOptions): bool
+    if install_count == 0 and upgrade_count == 0
+        return false
+    end if
+
+    println("")
+
+    if install_count > 0
+        println(color.bold("New packages to install:"))
+        mut i = 0
+        while i < install_count
+            println("  " + color.green("+") + " " + to_install[i])
+            i = i + 1
+        end while
+        println("")
+    end if
+
+    if upgrade_count > 0
+        println(color.bold("Packages to upgrade:"))
+        mut i = 0
+        while i < upgrade_count
+            println("  " + color.cyan("^") + " " + to_upgrade[i])
+            i = i + 1
+        end while
+        println("")
+    end if
+
+    let total = install_count + upgrade_count
+    println(color.dim("Total: " + convert.to_string(total) + " package(s)"))
+    println("")
+
+    return confirm("Proceed with installation?", opts)
+end confirm_install
+
+// Display packages to remove and ask for confirmation
+fn confirm_remove(to_remove: [string], count: int,
+                  dependents: [string], dep_count: int,
+                  opts: types.GlobalOptions): bool
+    if count == 0
+        return false
+    end if
+
+    println("")
+    println(color.bold("Packages to remove:"))
+
+    mut i = 0
+    while i < count
+        println("  " + color.red("-") + " " + to_remove[i])
+        i = i + 1
+    end while
+
+    if dep_count > 0
+        println("")
+        println(color.yellow("The following packages depend on these and will also be removed:"))
+        i = 0
+        while i < dep_count
+            println("  " + color.yellow("-") + " " + dependents[i])
+            i = i + 1
+        end while
+    end if
+
+    println("")
+    let total = count + dep_count
+    println(color.dim("Total: " + convert.to_string(total) + " package(s)"))
+    println("")
+
+    return confirm("Proceed with removal?", opts)
+end confirm_remove
+
+// Display build plan and ask for confirmation
+fn confirm_build_plan(packages: [string], versions: [string], count: int, target: string, opts: types.GlobalOptions): bool
+    if count == 0
+        return true
+    end if
+
+    println("")
+    println(color.bold("Build plan for '" + target + "':"))
+    println("")
+    println("Packages to build from source:")
+
+    mut i = 0
+    while i < count
+        mut line = "  " + color.green("+") + " " + packages[i]
+        if str.length(versions[i]) > 0
+            line = line + " " + color.dim(versions[i])
+        end if
+        println(line)
+        i = i + 1
+    end while
+
+    println("")
+    println(color.dim("Total: " + convert.to_string(count) + " package(s) to build"))
+    println("")
+
+    return confirm("Proceed with build?", opts)
+end confirm_build_plan
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 src/util/version.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/util/version.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,316 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: version.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: Semantic version parsing and comparison utilities
+
+******************************************************************************/
+
+module util.version
+
+import core.str
+
+export
+    // Version constraint parsing
+    type DepConstraint
+
+    // Compare two version strings semantically
+    // Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
+    fn compare_versions(v1: string, v2: string): int
+
+    // Check if v1 is newer than v2
+    fn is_newer(v1: string, v2: string): bool
+
+    // Parse a dependency string with optional version constraint
+    // e.g., "libfoo>=1.0" -> { name: "libfoo", op: ">=", version: "1.0" }
+    fn parse_dep_constraint(dep: string): DepConstraint
+
+    // Check if an installed version satisfies a version constraint
+    fn check_version_constraint(installed_version: string, op: string, required_version: string): bool
+
+    // Accessor functions for DepConstraint
+    fn constraint_name(c: DepConstraint): string
+    fn constraint_op(c: DepConstraint): string
+    fn constraint_version(c: DepConstraint): string
+    fn has_constraint(c: DepConstraint): bool
+
+    // Extract version from package filename
+    // e.g., "reef-0.1.10-1.pkg.tar.xz" -> "0.1.10"
+    fn extract_version_from_filename(filename: string, pkg_name: string): string
+
+    // Extract release number from package filename
+    // e.g., "reef-0.1.10-1.pkg.tar.xz" -> 1
+    fn extract_release_from_filename(filename: string): int
+end export
+
+// Parsed dependency constraint
+type DepConstraint = struct
+    name: string
+    op: string
+    version: string
+end DepConstraint
+
+// Compare two version strings semantically
+// Handles versions like "0.1.10" vs "0.1.7" correctly (0.1.10 > 0.1.7)
+fn compare_versions(v1: string, v2: string): int
+    // Parse version components
+    mut v1_parts: [int] = new [int](10)
+    mut v2_parts: [int] = new [int](10)
+
+    let v1_count = parse_version(v1, v1_parts, 10)
+    let v2_count = parse_version(v2, v2_parts, 10)
+
+    // Compare each component
+    let max_parts = max_int(v1_count, v2_count)
+
+    mut i = 0
+    while i < max_parts
+        let p1 = get_part(v1_parts, v1_count, i)
+        let p2 = get_part(v2_parts, v2_count, i)
+
+        if p1 > p2
+            return 1
+        elif p1 < p2
+            return -1
+        end if
+
+        i = i + 1
+    end while
+
+    return 0
+end compare_versions
+
+// Check if v1 is newer than v2
+fn is_newer(v1: string, v2: string): bool
+    return compare_versions(v1, v2) > 0
+end is_newer
+
+// Parse version string into integer components
+// "0.1.10" -> [0, 1, 10]
+fn parse_version(version: string, parts: [int], max_parts: int): int
+    mut count = 0
+    mut current = 0
+    mut in_number = false
+
+    mut i = 0
+    while i < str.length(version) and count < max_parts
+        let c = str.char_at(version, i)
+
+        if is_digit(c)
+            current = current * 10 + char_to_digit(c)
+            in_number = true
+        elif c == '.' or c == '-'
+            if in_number
+                parts[count] = current
+                count = count + 1
+                current = 0
+                in_number = false
+            end if
+        else
+            // Skip non-numeric characters (like 'alpha', 'beta', 'rc')
+            if in_number
+                parts[count] = current
+                count = count + 1
+                current = 0
+                in_number = false
+            end if
+        end if
+
+        i = i + 1
+    end while
+
+    // Don't forget the last number
+    if in_number and count < max_parts
+        parts[count] = current
+        count = count + 1
+    end if
+
+    return count
+end parse_version
+
+// Extract version from package filename
+// e.g., "reef-0.1.10-1.pkg.tar.xz" with pkg_name "reef" -> "0.1.10"
+fn extract_version_from_filename(filename: string, pkg_name: string): string
+    // Remove package name prefix and dash
+    let prefix_len = str.length(pkg_name) + 1
+
+    if str.length(filename) <= prefix_len
+        return ""
+    end if
+
+    // Get the part after "pkgname-"
+    let rest = str.substring(filename, prefix_len, str.length(filename) - prefix_len)
+
+    // Find the last dash before .pkg.tar.xz (that's the release separator)
+    // e.g., "0.1.10-1.pkg.tar.xz" -> version is "0.1.10"
+    let suffix = ".pkg.tar.xz"
+    let suffix_len = str.length(suffix)
+
+    if not str.ends_with(rest, suffix)
+        return ""
+    end if
+
+    // Remove suffix: "0.1.10-1.pkg.tar.xz" -> "0.1.10-1"
+    let ver_rel = str.substring(rest, 0, str.length(rest) - suffix_len)
+
+    // Find last dash (release separator)
+    let last_dash = str.last_index_of_char(ver_rel, '-')
+    if last_dash < 0
+        return ver_rel
+    end if
+
+    // Return version part: "0.1.10-1" -> "0.1.10"
+    return str.substring(ver_rel, 0, last_dash)
+end extract_version_from_filename
+
+// Extract release number from package filename
+// e.g., "reef-0.1.10-1.pkg.tar.xz" -> 1
+fn extract_release_from_filename(filename: string): int
+    let suffix = ".pkg.tar.xz"
+
+    if not str.ends_with(filename, suffix)
+        return 1
+    end if
+
+    // Remove suffix
+    let without_suffix = str.substring(filename, 0, str.length(filename) - str.length(suffix))
+
+    // Find last dash
+    let last_dash = str.last_index_of_char(without_suffix, '-')
+    if last_dash < 0
+        return 1
+    end if
+
+    // Extract release number
+    let release_str = str.substring(without_suffix, last_dash + 1, str.length(without_suffix) - last_dash - 1)
+
+    return parse_int(release_str)
+end extract_release_from_filename
+
+// Helper: check if character is a digit
+fn is_digit(c: char): bool
+    return c >= '0' and c <= '9'
+end is_digit
+
+// Helper: convert digit character to integer
+fn char_to_digit(c: char): int
+    return c - '0'
+end char_to_digit
+
+// Helper: get part at index, or 0 if out of bounds
+fn get_part(parts: [int], count: int, idx: int): int
+    if idx < count
+        return parts[idx]
+    end if
+    return 0
+end get_part
+
+// Helper: max of two integers
+fn max_int(a: int, b: int): int
+    if a > b
+        return a
+    end if
+    return b
+end max_int
+
+// Helper: parse string to int
+fn parse_int(s: string): int
+    mut result = 0
+    mut i = 0
+    while i < str.length(s)
+        let c = str.char_at(s, i)
+        if is_digit(c)
+            result = result * 10 + char_to_digit(c)
+        else
+            break
+        end if
+        i = i + 1
+    end while
+    return result
+end parse_int
+
+// Parse a dependency string into name, operator, and version
+// Handles: "libfoo", "libfoo>=1.0", "libfoo<=2.0", "libfoo>1.0", "libfoo<2.0", "libfoo=1.0"
+fn parse_dep_constraint(dep: string): DepConstraint
+    let len = str.length(dep)
+
+    mut i = 0
+    while i < len
+        let c = str.char_at(dep, i)
+
+        if c == '>' or c == '<' or c == '='
+            let name = str.substring(dep, 0, i)
+
+            // Check for two-char operators (>=, <=)
+            if i + 1 < len
+                let next = str.char_at(dep, i + 1)
+                if next == '='
+                    let op = str.substring(dep, i, 2)
+                    let ver = str.substring(dep, i + 2, len - i - 2)
+                    return DepConstraint{ name: name, op: op, version: ver }
+                end if
+            end if
+
+            // Single char operator (>, <, =)
+            let op = str.substring(dep, i, 1)
+            let ver = str.substring(dep, i + 1, len - i - 1)
+            return DepConstraint{ name: name, op: op, version: ver }
+        end if
+
+        i = i + 1
+    end while
+
+    // No constraint
+    return DepConstraint{ name: dep, op: "", version: "" }
+end parse_dep_constraint
+
+// Check if an installed version satisfies a version constraint
+fn check_version_constraint(installed_version: string, op: string, required_version: string): bool
+    if str.length(op) == 0
+        return true
+    end if
+
+    let cmp = compare_versions(installed_version, required_version)
+
+    if str.equals(op, ">=")
+        return cmp >= 0
+    elif str.equals(op, "<=")
+        return cmp <= 0
+    elif str.equals(op, ">")
+        return cmp > 0
+    elif str.equals(op, "<")
+        return cmp < 0
+    elif str.equals(op, "=")
+        return cmp == 0
+    end if
+
+    return true
+end check_version_constraint
+
+// Accessor functions
+fn constraint_name(c: DepConstraint): string
+    return c.name
+end constraint_name
+
+fn constraint_op(c: DepConstraint): string
+    return c.op
+end constraint_op
+
+fn constraint_version(c: DepConstraint): string
+    return c.version
+end constraint_version
+
+fn has_constraint(c: DepConstraint): bool
+    return str.length(c.op) > 0
+end has_constraint
+
+end module
diff -r 000000000000 -r d0a96d73e9e1 test/http_test.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/http_test.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,53 @@
+// Minimal HTTP download test for debugging
+// Tests net.http.http_download_file() which appears to hang on illumos
+
+import net.http
+import io.file
+
+proc main()
+    println("=== HTTP Download Test ===")
+    println("")
+
+    // Test 1: Simple HTTPS URL (zlib - small file)
+    let url1 = "https://zlib.net/zlib-1.3.1.tar.gz"
+    let dest1 = "/tmp/http_test_zlib.tar.gz"
+
+    println("Test 1: zlib download (1.5MB)")
+    println("URL: " + url1)
+    println("Calling http_download_file()...")
+
+    let result1 = http.http_download_file(url1, dest1)
+
+    if result1
+        println("SUCCESS: Downloaded to " + dest1)
+        if file.fileExists(dest1)
+            println("File exists: true")
+        end if
+    else
+        println("FAILED: http_download_file returned false")
+    end if
+
+    println("")
+
+    // Test 2: GitHub URL (requires redirect handling)
+    let url2 = "https://github.com/vim/vim/archive/refs/tags/v9.1.0.tar.gz"
+    let dest2 = "/tmp/http_test_vim.tar.gz"
+
+    println("Test 2: GitHub download with redirects (17MB)")
+    println("URL: " + url2)
+    println("Calling http_download_file()...")
+
+    let result2 = http.http_download_file(url2, dest2)
+
+    if result2
+        println("SUCCESS: Downloaded to " + dest2)
+        if file.fileExists(dest2)
+            println("File exists: true")
+        end if
+    else
+        println("FAILED: http_download_file returned false")
+    end if
+
+    println("")
+    println("=== Test Complete ===")
+end main
diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/apptest/build.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/base/apptest/build.sh	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,34 @@
+#!/bin/sh
+# Build script for apptest
+# A test application that uses libtest
+
+set -e
+
+echo "Building apptest..."
+
+# Create source file
+mkdir -p src
+
+cat > src/apptest.c << 'EOF'
+#include <stdio.h>
+#include <libtest.h>
+
+int main(void) {
+    printf("apptest v1.0.0\n");
+    printf("Using libtest: %s\n", libtest_version());
+    printf("Message: %s\n", libtest_greet());
+    printf("\nDependency chain validated successfully!\n");
+    return 0;
+}
+EOF
+
+# Compile - link against libtest
+echo "Compiling apptest..."
+gcc ${CFLAGS} -I/usr/include -o apptest src/apptest.c -L/usr/lib -ltest
+
+# Install to DESTDIR
+echo "Installing to ${DESTDIR}..."
+mkdir -p "${DESTDIR}/usr/bin"
+install -m 755 apptest "${DESTDIR}/usr/bin/apptest"
+
+echo "Build complete!"
diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/apptest/package.toml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/base/apptest/package.toml	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,26 @@
+# apptest - Simple Test Application
+# Depends on libtest - for testing dependency resolution
+
+[package]
+name = "apptest"
+version = "1.0.0"
+release = 1
+description = "A test application that depends on libtest"
+url = "https://github.com/leafscale/zygaena"
+license = "BSD-2-Clause"
+maintainer = "Chris Tusa <chris.tusa@leafscale.com>"
+arch = "x86_64"
+
+[dependencies]
+# This is the key test - apptest depends on libtest
+runtime = ["libtest"]
+build = ["libtest"]
+
+[sources]
+# No external sources - we create inline
+urls = []
+checksums = []
+
+[build]
+parallel = true
+jobs = 1
diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/hello/build.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/base/hello/build.sh	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,46 @@
+#!/bin/sh
+# Build script for hello package
+# This is a test package that creates a simple hello world program
+
+set -e
+
+echo "Building hello world test package..."
+
+# Create source directory
+mkdir -p src
+
+# Create simple hello.c
+cat > src/hello.c << 'EOF'
+#include <stdio.h>
+
+int main(void) {
+    printf("Hello from Coral package manager!\n");
+    printf("If you see this, the build system is working.\n");
+    return 0;
+}
+EOF
+
+# Compile
+echo "Compiling hello.c..."
+gcc ${CFLAGS} -o hello src/hello.c
+
+# Install to DESTDIR
+echo "Installing to ${DESTDIR}..."
+mkdir -p "${DESTDIR}/usr/bin"
+install -m 755 hello "${DESTDIR}/usr/bin/hello"
+
+# Create a simple man page
+mkdir -p "${DESTDIR}/usr/share/man/man1"
+cat > "${DESTDIR}/usr/share/man/man1/hello.1" << 'EOF'
+.TH HELLO 1 "2025" "Coral" "User Commands"
+.SH NAME
+hello \- a test program for coral package manager
+.SH SYNOPSIS
+.B hello
+.SH DESCRIPTION
+A simple hello world program used to test the Coral package build system.
+.SH AUTHOR
+Chris Tusa <chris.tusa@leafscale.com>
+EOF
+
+echo "Build complete!"
diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/hello/package.toml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/base/hello/package.toml	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,25 @@
+# Hello World Package - Test Port
+# This is a simple test package to verify coral build works
+
+[package]
+name = "hello"
+version = "1.0.1"
+release = 1
+description = "A simple hello world program for testing coral"
+url = "https://github.com/leafscale/zygaena"
+license = "MIT"
+maintainer = "Chris Tusa <chris.tusa@leafscale.com>"
+arch = "x86_64"
+
+[dependencies]
+runtime = []
+build = []
+
+[sources]
+# No external sources - we include the source directly
+urls = []
+checksums = []
+
+[build]
+parallel = true
+jobs = 1
diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/tree/build.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/base/tree/build.sh	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,37 @@
+#!/bin/sh
+# Build script for tree
+# A directory listing utility
+
+set -e
+
+echo "Building tree..."
+
+# Enter source directory (GitHub archives extract with different name)
+cd tree-2.1.1
+
+# On illumos, use GNU make
+MAKE=gmake
+if [ "$(uname -s)" != "SunOS" ]; then
+    MAKE=make
+fi
+
+# Build with appropriate flags for illumos
+case "$(uname -s)" in
+    SunOS|illumos)
+        # illumos needs specific flags
+        $MAKE CC=gcc CFLAGS="${CFLAGS} -D_XOPEN_SOURCE=600" prefix=/usr
+        ;;
+    *)
+        $MAKE CC=gcc CFLAGS="${CFLAGS}" prefix=/usr
+        ;;
+esac
+
+# Install to DESTDIR
+echo "Installing to ${DESTDIR}..."
+mkdir -p "${DESTDIR}/usr/bin"
+mkdir -p "${DESTDIR}/usr/share/man/man1"
+
+install -m 755 tree "${DESTDIR}/usr/bin/tree"
+install -m 644 doc/tree.1 "${DESTDIR}/usr/share/man/man1/tree.1"
+
+echo "Build complete!"
diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/tree/package.toml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/base/tree/package.toml	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,25 @@
+# Tree Package - Directory Listing Utility
+# A simple C program for testing coral with real source
+
+[package]
+name = "tree"
+version = "2.1.1"
+release = 1
+description = "List directory contents in a tree-like format"
+url = "http://mama.indstate.edu/users/ice/tree/"
+license = "GPL-2.0"
+maintainer = "Chris Tusa <chris.tusa@leafscale.com>"
+arch = "x86_64"
+
+[dependencies]
+runtime = []
+build = []
+
+[sources]
+# Real source from upstream (single-line format for TOML parser compatibility)
+urls = ["https://github.com/Old-Man-Programmer/tree/archive/refs/tags/2.1.1.tar.gz"]
+checksums = ["SKIP"]
+
+[build]
+parallel = true
+jobs = 1
diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/vim/build.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/base/vim/build.sh	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,39 @@
+#!/bin/sh
+# Build script for vim
+# Vi IMproved text editor
+
+set -e
+
+echo "Building vim..."
+
+cd vim-9.1.0000
+
+# On illumos, use GNU make
+MAKE=gmake
+if [ "$(uname -s)" != "SunOS" ]; then
+    MAKE=make
+fi
+
+# Configure with minimal features for testing
+# (Full vim would have more options)
+./configure \
+    --prefix=/usr \
+    --with-features=normal \
+    --enable-multibyte \
+    --disable-gui \
+    --without-x \
+    --disable-netbeans \
+    --enable-cscope
+
+# Build
+$MAKE ${MAKEFLAGS}
+
+# Install to DESTDIR
+echo "Installing to ${DESTDIR}..."
+$MAKE DESTDIR="${DESTDIR}" install
+
+# Create vi symlink
+cd "${DESTDIR}/usr/bin"
+ln -sf vim vi 2>/dev/null || true
+
+echo "Build complete!"
diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/vim/package.toml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/base/vim/package.toml	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,32 @@
+# Vim - Vi IMproved
+# The classic text editor
+
+[package]
+name = "vim"
+version = "9.1.0"
+release = 1
+description = "Vi IMproved - enhanced vi text editor"
+url = "https://www.vim.org/"
+license = "Vim"
+maintainer = "Chris Tusa <chris.tusa@leafscale.com>"
+arch = "x86_64"
+
+# vim provides the editor and vi alternatives
+alternatives = ["editor", "vi"]
+
+[dependencies]
+# Depends on ncurses for terminal handling
+runtime = ["ncurses"]
+build = ["ncurses"]
+
+[[source]]
+file = "vim-9.1.0.tar.gz"
+urls = [
+    "https://github.com/vim/vim/archive/refs/tags/v9.1.0000.tar.gz",
+    "https://ftp.nluug.nl/pub/vim/unix/vim-9.1.tar.bz2"
+]
+checksum = "SKIP"
+
+[build]
+parallel = true
+jobs = 0
diff -r 000000000000 -r d0a96d73e9e1 test/ports/devel/libtest/build.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/devel/libtest/build.sh	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,62 @@
+#!/bin/sh
+# Build script for libtest
+# A simple test library for validating coral dependency resolution
+
+set -e
+
+echo "Building libtest..."
+
+# Create source files
+mkdir -p src
+
+cat > src/libtest.h << 'EOF'
+#ifndef LIBTEST_H
+#define LIBTEST_H
+
+/* Return a greeting message */
+const char* libtest_greet(void);
+
+/* Return the library version */
+const char* libtest_version(void);
+
+#endif /* LIBTEST_H */
+EOF
+
+cat > src/libtest.c << 'EOF'
+#include "libtest.h"
+
+const char* libtest_greet(void) {
+    return "Hello from libtest!";
+}
+
+const char* libtest_version(void) {
+    return "1.0.0";
+}
+EOF
+
+# Compile shared library
+echo "Compiling libtest..."
+gcc ${CFLAGS} -fPIC -c src/libtest.c -o libtest.o
+gcc -shared -o libtest.so.1.0.0 libtest.o
+
+# Create version symlinks
+ln -sf libtest.so.1.0.0 libtest.so.1
+ln -sf libtest.so.1 libtest.so
+
+# Install to DESTDIR
+echo "Installing to ${DESTDIR}..."
+mkdir -p "${DESTDIR}/usr/lib"
+mkdir -p "${DESTDIR}/usr/include"
+
+# Save current directory (build dir)
+BUILD_DIR=$(pwd)
+
+install -m 755 libtest.so.1.0.0 "${DESTDIR}/usr/lib/"
+cd "${DESTDIR}/usr/lib"
+ln -sf libtest.so.1.0.0 libtest.so.1
+ln -sf libtest.so.1 libtest.so
+
+# Install header from saved build directory
+install -m 644 "${BUILD_DIR}/src/libtest.h" "${DESTDIR}/usr/include/"
+
+echo "Build complete!"
diff -r 000000000000 -r d0a96d73e9e1 test/ports/devel/libtest/package.toml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/devel/libtest/package.toml	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,28 @@
+# libtest - Simple Test Library
+# For testing dependency resolution
+
+[package]
+name = "libtest"
+version = "1.0.1"
+release = 1
+description = "A simple test library for validating coral dependencies"
+url = "https://github.com/leafscale/zygaena"
+license = "BSD-2-Clause"
+maintainer = "Chris Tusa <chris.tusa@leafscale.com>"
+arch = "x86_64"
+
+# Provides the testlib alternative
+alternatives = ["testlib"]
+
+[dependencies]
+runtime = []
+build = []
+
+[sources]
+# No external sources - we create inline
+urls = []
+checksums = []
+
+[build]
+parallel = true
+jobs = 1
diff -r 000000000000 -r d0a96d73e9e1 test/ports/devel/ncurses/build.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/devel/ncurses/build.sh	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,48 @@
+#!/bin/sh
+# Build script for ncurses
+# Terminal UI library
+
+set -e
+
+echo "Building ncurses..."
+
+cd ncurses-6.4
+
+# On illumos, use GNU make
+MAKE=gmake
+if [ "$(uname -s)" != "SunOS" ]; then
+    MAKE=make
+fi
+
+# Configure with common options
+./configure \
+    --prefix=/usr \
+    --with-shared \
+    --with-cxx-shared \
+    --without-debug \
+    --without-normal \
+    --enable-widec \
+    --enable-pc-files \
+    --with-pkg-config-libdir=/usr/lib/pkgconfig
+
+# Build
+$MAKE ${MAKEFLAGS}
+
+# Install to DESTDIR
+echo "Installing to ${DESTDIR}..."
+$MAKE DESTDIR="${DESTDIR}" install
+
+# Create compatibility symlinks for non-wide-char programs
+# Many programs expect -lncurses, but we built -lncursesw
+cd "${DESTDIR}/usr/lib"
+for lib in ncurses form panel menu; do
+    ln -sf lib${lib}w.so lib${lib}.so 2>/dev/null || true
+    ln -sf ${lib}w.pc /usr/lib/pkgconfig/${lib}.pc 2>/dev/null || true
+done
+
+# Also provide curses.h symlink
+cd "${DESTDIR}/usr/include"
+ln -sf ncursesw/curses.h curses.h 2>/dev/null || true
+ln -sf ncursesw/ncurses.h ncurses.h 2>/dev/null || true
+
+echo "Build complete!"
diff -r 000000000000 -r d0a96d73e9e1 test/ports/devel/ncurses/package.toml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/devel/ncurses/package.toml	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,31 @@
+# ncurses - Terminal UI Library
+# Foundation library for terminal applications
+
+[package]
+name = "ncurses"
+version = "6.4"
+release = 1
+description = "Terminal UI library for Unix-like systems"
+url = "https://invisible-island.net/ncurses/"
+license = "MIT"
+maintainer = "Chris Tusa <chris.tusa@leafscale.com>"
+arch = "x86_64"
+
+# ncurses provides the curses alternative
+alternatives = ["curses"]
+
+[dependencies]
+runtime = []
+build = []
+
+[sources]
+urls = [
+    "https://ftp.gnu.org/gnu/ncurses/ncurses-6.4.tar.gz"
+]
+checksums = [
+    "SKIP"
+]
+
+[build]
+parallel = true
+jobs = 0
diff -r 000000000000 -r d0a96d73e9e1 test/ports/groups/base-devel.toml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ports/groups/base-devel.toml	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,18 @@
+# Base development tools package group
+# Install with: coral install @base-devel
+
+[group]
+name = "base-devel"
+description = "Base development tools for building packages"
+packages = [
+    "gcc",
+    "gmake",
+    "binutils",
+    "autoconf",
+    "automake",
+    "libtool",
+    "pkg-config",
+    "m4",
+    "bison",
+    "flex"
+]
diff -r 000000000000 -r d0a96d73e9e1 tools/mkchroot/.gitignore
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/mkchroot/.gitignore	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,16 @@
+# Build artifacts
+build/
+*.c
+*.o
+*.so
+*.exe
+
+# Editor
+.vscode/
+.idea/
+*.swp
+*~
+
+# OS
+.DS_Store
+Thumbs.db
diff -r 000000000000 -r d0a96d73e9e1 tools/mkchroot/README.md
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/mkchroot/README.md	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,20 @@
+# mkchroot
+
+A Reef project.
+
+## Building
+
+```bash
+cd mkchroot
+reefc build
+```
+
+## Running
+
+```bash
+reefc run
+```
+
+## About
+
+Created with Reef Compiler 0.1.15
diff -r 000000000000 -r d0a96d73e9e1 tools/mkchroot/reef.toml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/mkchroot/reef.toml	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,17 @@
+[package]
+name = "mkchroot"
+version = "0.1.0"
+author = "Chris Tusa <chris.tusa@leafscale.com>"
+description = "Create chrooted test environment for Coral package manager"
+license = "MIT"
+url = "https://github.com/leafscale/coral"
+
+[build]
+entry = "src/main.reef"
+output = "mkchroot"
+output_dir = "build"
+source_dirs = ["src"]
+
+[docs]
+output = "docs"
+include_private = false
diff -r 000000000000 -r d0a96d73e9e1 tools/mkchroot/src/main.reef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/mkchroot/src/main.reef	Tue Jun 09 17:28:44 2026 -0500
@@ -0,0 +1,626 @@
+/******************************************************************************
+               __               ____                __
+              / /   ___  ____ _/ __/_____________ _/ /__
+             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
+            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
+           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
+
+   (C)opyright 2025, Leafscale, LLC -  https://www.leafscale.com
+
+   Project: Zygaena
+  Filename: main.reef
+   Authors: Chris Tusa <chris.tusa@leafscale.com>
+   License: <see LICENSE file included with this source code>
+Description: mkchroot - Create chrooted test environment for Coral
+
+******************************************************************************/
+
+import sys.args
+import sys.process
+import io.file
+import io.dir
+import io.path
+import core.str
+
+// Default chroot base path
+const DEFAULT_CHROOT: string = "/tmp/coral-chroot"
+
+// Ports source location
+const PORTS_SOURCE: string = "/home/ctusa/repos/zygaena-ports"
+
+fn main(): int
+    println("mkchroot - Coral Test Environment Builder")
+    println("")
+
+    // Parse command line for custom path and coral-bin option
+    let chroot_path = get_chroot_path()
+    let coral_bin = get_coral_bin()
+
+    // Handle help
+    if chroot_path == "--help" or chroot_path == "-h"
+        print_usage()
+        return 0
+    end if
+
+    println("Creating chroot at: " + chroot_path)
+    println("")
+
+    // Check if chroot already exists
+    if dir.dir_exists(chroot_path)
+        println("WARNING: Chroot directory already exists")
+        println("Removing existing chroot...")
+        let rm_cmd = "rm -rf \"" + chroot_path + "\""
+        let rm_pid = process.process_spawn_shell(rm_cmd)
+        if rm_pid > 0
+            process.process_wait(rm_pid)
+        end if
+    end if
+
+    // Create base filesystem structure
+    if not create_filesystem(chroot_path)
+        println("ERROR: Failed to create filesystem structure")
+        return 1
+    end if
+
+    // Create Coral-specific directories
+    if not create_coral_dirs(chroot_path)
+        println("ERROR: Failed to create Coral directories")
+        return 1
+    end if
+
+    // Create default configuration
+    if not create_config(chroot_path)
+        println("ERROR: Failed to create configuration")
+        return 1
+    end if
+
+    // Copy ports tree
+    if not copy_ports(chroot_path)
+        println("ERROR: Failed to copy ports tree")
+        return 1
+    end if
+
+    // Copy coral binary if specified
+    if str.length(coral_bin) > 0
+        if not install_coral(chroot_path, coral_bin)
+            println("WARNING: Failed to copy coral binary")
+        end if
+    end if
+
+    // Generate setup and teardown scripts
+    if not create_setup_script(chroot_path)
+        println("WARNING: Failed to create setup script")
+    end if
+
+    if not create_teardown_script(chroot_path)
+        println("WARNING: Failed to create teardown script")
+    end if
+
+    println("")
+    println("Chroot environment created successfully!")
+    println("")
+    println("=== Usage ===")
+    println("")
+    println("Option 1: Use --root flag (no mounts needed):")
+    println("  coral --root " + chroot_path + " db init")
+    println("  coral --root " + chroot_path + " install zlib")
+    println("  coral --root " + chroot_path + " list")
+    println("")
+    println("Option 2: Enter chroot (requires setup):")
+    println("  1. Setup mounts:  sudo " + chroot_path + "-setup.sh " + chroot_path)
+    println("  2. Enter chroot:  sudo chroot " + chroot_path + " /bin/bash")
+    println("  3. Init database: coral db init")
+    println("  4. Build/install: coral build core/zlib")
+    println("  5. Exit:          exit")
+    println("  6. Cleanup:       sudo " + chroot_path + "-teardown.sh " + chroot_path)
+    println("")
+    println("=== Directory layout ===")
+    println("  " + chroot_path + "/etc/coral/         - Configuration")
+    println("  " + chroot_path + "/var/lib/coral/     - Package database")
+    println("  " + chroot_path + "/usr/zports/        - Ports tree")
+    println("")
+    println("=== Generated scripts ===")
+    println("  " + chroot_path + "-setup.sh    - Mount /dev, /proc, /sys, and host binaries")
+    println("  " + chroot_path + "-teardown.sh - Unmount all chroot mounts")
+    println("")
+
+    return 0
+end main
+
+fn get_chroot_path(): string
+    let argc = args.count()
+
+    // Check for --path argument
+    if argc >= 3
+        if args.get(1) == "--path"
+            return args.get(2)
+        end if
+    end if
+
+    // Check for positional argument (skip if it's a flag)
+    if argc >= 2
+        let arg = args.get(1)
+        if not str.starts_with(arg, "-")
+            return arg
+        end if
+    end if
+
+    // Check for positional after flags
+    mut i = 1
+    while i < argc
+        let arg = args.get(i)
+        if arg == "--coral-bin" and i + 1 < argc
+            i = i + 2  // Skip flag and value
+            continue
+        end if
+        if not str.starts_with(arg, "-")
+            return arg
+        end if
+        i = i + 1
+    end while
+
+    return DEFAULT_CHROOT
+end get_chroot_path
+
+fn get_coral_bin(): string
+    let argc = args.count()
+
+    mut i = 1
+    while i < argc
+        if args.get(i) == "--coral-bin" and i + 1 < argc
+            return args.get(i + 1)
+        end if
+        i = i + 1
+    end while
+
+    return ""
+end get_coral_bin
+
+fn install_coral(base: string, coral_path: string): bool
+    println("Installing coral binary from " + coral_path + "...")
+
+    // Check if source exists
+    if not file.fileExists(coral_path)
+        println("  ERROR: Coral binary not found at " + coral_path)
+        return false
+    end if
+
+    let dst = base + "/usr/local/bin/coral"
+    let cmd = "cp \"" + coral_path + "\" \"" + dst + "\" && chmod +x \"" + dst + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid > 0
+        let exit_code = process.process_wait(pid)
+        if exit_code == 0
+            println("  Installed coral to " + dst)
+            return true
+        end if
+    end if
+
+    return false
+end install_coral
+
+fn create_setup_script(base: string): bool
+    let script_path = base + "-setup.sh"
+    println("Creating " + script_path + "...")
+
+    let script_content = "#!/bin/sh\n" +
+        "# Setup script for coral chroot environment\n" +
+        "# Generated by mkchroot\n" +
+        "\n" +
+        "CHROOT=\"$1\"\n" +
+        "\n" +
+        "if [ -z \"$CHROOT\" ]; then\n" +
+        "    echo \"Usage: $0 <chroot-path>\"\n" +
+        "    exit 1\n" +
+        "fi\n" +
+        "\n" +
+        "if [ ! -d \"$CHROOT\" ]; then\n" +
+        "    echo \"Error: Chroot directory does not exist: $CHROOT\"\n" +
+        "    exit 1\n" +
+        "fi\n" +
+        "\n" +
+        "echo \"Setting up mounts for chroot: $CHROOT\"\n" +
+        "\n" +
+        "# Mount essential filesystems\n" +
+        "mount --bind /dev \"$CHROOT/dev\" 2>/dev/null && echo \"  Mounted /dev\"\n" +
+        "mount --bind /proc \"$CHROOT/proc\" 2>/dev/null && echo \"  Mounted /proc\"\n" +
+        "mount -t sysfs sysfs \"$CHROOT/sys\" 2>/dev/null && echo \"  Mounted /sys\"\n" +
+        "\n" +
+        "# Mount host binaries and libraries for building\n" +
+        "mount --bind /usr/bin \"$CHROOT/usr/bin\" 2>/dev/null && echo \"  Mounted /usr/bin\"\n" +
+        "mount --bind /usr/lib \"$CHROOT/usr/lib\" 2>/dev/null && echo \"  Mounted /usr/lib\"\n" +
+        "mount --bind /lib \"$CHROOT/lib\" 2>/dev/null && echo \"  Mounted /lib\"\n" +
+        "\n" +
+        "# illumos-specific mounts\n" +
+        "[ -d /lib/64 ] && mount --bind /lib/64 \"$CHROOT/lib/64\" 2>/dev/null && echo \"  Mounted /lib/64\"\n" +
+        "[ -d /usr/lib/64 ] && mount --bind /usr/lib/64 \"$CHROOT/usr/lib/64\" 2>/dev/null && echo \"  Mounted /usr/lib/64\"\n" +
+        "\n" +
+        "echo \"\"\n" +
+        "echo \"Chroot is ready. Enter with:\"\n" +
+        "echo \"  sudo chroot $CHROOT /bin/bash\"\n"
+
+    if not file.writeFile(script_path, script_content)
+        return false
+    end if
+
+    // Make executable
+    let chmod_cmd = "chmod +x \"" + script_path + "\""
+    let pid = process.process_spawn_shell(chmod_cmd)
+    if pid > 0
+        process.process_wait(pid)
+    end if
+
+    println("  Created " + script_path)
+    return true
+end create_setup_script
+
+fn create_teardown_script(base: string): bool
+    let script_path = base + "-teardown.sh"
+    println("Creating " + script_path + "...")
+
+    let script_content = "#!/bin/sh\n" +
+        "# Teardown script for coral chroot environment\n" +
+        "# Generated by mkchroot\n" +
+        "\n" +
+        "CHROOT=\"$1\"\n" +
+        "\n" +
+        "if [ -z \"$CHROOT\" ]; then\n" +
+        "    echo \"Usage: $0 <chroot-path>\"\n" +
+        "    exit 1\n" +
+        "fi\n" +
+        "\n" +
+        "echo \"Unmounting chroot: $CHROOT\"\n" +
+        "\n" +
+        "# Unmount in reverse order\n" +
+        "umount \"$CHROOT/usr/lib/64\" 2>/dev/null && echo \"  Unmounted /usr/lib/64\"\n" +
+        "umount \"$CHROOT/lib/64\" 2>/dev/null && echo \"  Unmounted /lib/64\"\n" +
+        "umount \"$CHROOT/lib\" 2>/dev/null && echo \"  Unmounted /lib\"\n" +
+        "umount \"$CHROOT/usr/lib\" 2>/dev/null && echo \"  Unmounted /usr/lib\"\n" +
+        "umount \"$CHROOT/usr/bin\" 2>/dev/null && echo \"  Unmounted /usr/bin\"\n" +
+        "umount \"$CHROOT/sys\" 2>/dev/null && echo \"  Unmounted /sys\"\n" +
+        "umount \"$CHROOT/proc\" 2>/dev/null && echo \"  Unmounted /proc\"\n" +
+        "umount \"$CHROOT/dev\" 2>/dev/null && echo \"  Unmounted /dev\"\n" +
+        "\n" +
+        "echo \"\"\n" +
+        "echo \"Chroot cleanup complete.\"\n"
+
+    if not file.writeFile(script_path, script_content)
+        return false
+    end if
+
+    // Make executable
+    let chmod_cmd = "chmod +x \"" + script_path + "\""
+    let pid = process.process_spawn_shell(chmod_cmd)
+    if pid > 0
+        process.process_wait(pid)
+    end if
+
+    println("  Created " + script_path)
+    return true
+end create_teardown_script
+
+fn create_filesystem(base: string): bool
+    println("Creating base filesystem structure...")
+
+    // Create all directories using a single mkdir -p command
+    let cmd = "mkdir -p " +
+        "\"" + base + "/bin\" " +
+        "\"" + base + "/sbin\" " +
+        "\"" + base + "/lib\" " +
+        "\"" + base + "/lib/64\" " +
+        "\"" + base + "/tmp\" " +
+        "\"" + base + "/dev\" " +
+        "\"" + base + "/proc\" " +
+        "\"" + base + "/usr\" " +
+        "\"" + base + "/usr/bin\" " +
+        "\"" + base + "/usr/sbin\" " +
+        "\"" + base + "/usr/lib\" " +
+        "\"" + base + "/usr/lib/64\" " +
+        "\"" + base + "/usr/include\" " +
+        "\"" + base + "/usr/share\" " +
+        "\"" + base + "/usr/share/man\" " +
+        "\"" + base + "/usr/share/man/man1\" " +
+        "\"" + base + "/usr/share/man/man3\" " +
+        "\"" + base + "/usr/share/man/man5\" " +
+        "\"" + base + "/usr/share/man/man8\" " +
+        "\"" + base + "/usr/share/doc\" " +
+        "\"" + base + "/usr/local\" " +
+        "\"" + base + "/usr/local/bin\" " +
+        "\"" + base + "/usr/local/lib\" " +
+        "\"" + base + "/etc\" " +
+        "\"" + base + "/etc/default\" " +
+        "\"" + base + "/etc/init.d\" " +
+        "\"" + base + "/var\" " +
+        "\"" + base + "/var/lib\" " +
+        "\"" + base + "/var/log\" " +
+        "\"" + base + "/var/run\" " +
+        "\"" + base + "/var/tmp\" " +
+        "\"" + base + "/var/cache\" " +
+        "\"" + base + "/opt\" " +
+        "\"" + base + "/home\" " +
+        "\"" + base + "/root\""
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid > 0
+        process.process_wait(pid)
+    end if
+
+    println("  Created 35 system directories")
+    return true
+end create_filesystem
+
+fn create_coral_dirs(base: string): bool
+    println("Creating Coral directory structure...")
+
+    let cmd = "mkdir -p " +
+        "\"" + base + "/etc/coral\" " +
+        "\"" + base + "/etc/coral/keys\" " +
+        "\"" + base + "/etc/coral/repos.d\" " +
+        "\"" + base + "/var/lib/coral\" " +
+        "\"" + base + "/var/lib/coral/installed\" " +
+        "\"" + base + "/var/lib/coral/db\" " +
+        "\"" + base + "/var/lib/coral/repos\" " +
+        "\"" + base + "/var/lib/coral/trusted-keys\" " +
+        "\"" + base + "/usr/zports\" " +
+        "\"" + base + "/usr/zports/groups\" " +
+        "\"" + base + "/usr/zports/pkgs\" " +
+        "\"" + base + "/usr/zports/pkgs/sources\" " +
+        "\"" + base + "/usr/zports/pkgs/packages\" " +
+        "\"" + base + "/usr/zports/pkgs/build\""
+
+    let pid = process.process_spawn_shell(cmd)
+    if pid > 0
+        process.process_wait(pid)
+    end if
+
+    println("  Created 14 Coral directories")
+    return true
+end create_coral_dirs
+
+fn create_config(base: string): bool
+    println("Creating default configuration...")
+
+    // Create coral.conf
+    let conf_path = base + "/etc/coral/coral.conf"
+    let conf_content = "# Coral Package Manager Configuration\n" +
+        "# Generated by mkchroot for testing\n" +
+        "\n" +
+        "[paths]\n" +
+        "# Ports tree location\n" +
+        "ports_dir = /usr/zports\n" +
+        "\n" +
+        "# Build directories\n" +
+        "sources_dir = /usr/zports/pkgs/sources\n" +
+        "packages_dir = /usr/zports/pkgs/packages\n" +
+        "build_dir = /usr/zports/pkgs/build\n" +
+        "\n" +
+        "# Package database\n" +
+        "db_dir = /var/lib/coral/installed\n" +
+        "index_dir = /var/lib/coral/db\n" +
+        "\n" +
+        "[build]\n" +
+        "# Number of parallel jobs (0 = auto)\n" +
+        "jobs = 0\n" +
+        "\n" +
+        "# Keep build directory after successful build\n" +
+        "keep_build = false\n" +
+        "\n" +
+        "[network]\n" +
+        "# Download timeout in seconds\n" +
+        "timeout = 30\n" +
+        "\n" +
+        "# Retry count for failed downloads\n" +
+        "retries = 3\n"
+
+    if not file.writeFile(conf_path, conf_content)
+        println("  Failed to write coral.conf")
+        return false
+    end if
+    println("  Created coral.conf")
+
+    // Create empty repos.d placeholder
+    let repos_path = base + "/etc/coral/repos.d/default.repo"
+    let repos_content = "# Default Repository Configuration\n" +
+        "# Add repository URLs here\n" +
+        "\n" +
+        "[zygaena-main]\n" +
+        "name = Zygaena Main Repository\n" +
+        "url = https://pkg.zygaena.org/main\n" +
+        "enabled = true\n" +
+        "priority = 10\n"
+
+    if not file.writeFile(repos_path, repos_content)
+        println("  Failed to write default.repo")
+        return false
+    end if
+    println("  Created default.repo")
+
+    return true
+end create_config
+
+fn copy_ports(base: string): bool
+    println("Copying ports tree from " + PORTS_SOURCE + "...")
+
+    // Check if source exists
+    if not dir.dir_exists(PORTS_SOURCE)
+        println("  WARNING: Ports source not found at " + PORTS_SOURCE)
+        println("  Creating empty ports tree structure...")
+        return create_empty_ports(base)
+    end if
+
+    // Copy core category
+    let core_src = PORTS_SOURCE + "/core"
+    let core_dst = base + "/usr/zports/groups/core"
+    if dir.dir_exists(core_src)
+        println("  Copying core category...")
+        let cmd1 = "cp -r \"" + core_src + "\" \"" + core_dst + "\""
+        let pid1 = process.process_spawn_shell(cmd1)
+        if pid1 > 0
+            process.process_wait(pid1)
+        end if
+    end if
+
+    // Copy base category
+    let base_src = PORTS_SOURCE + "/base"
+    let base_dst = base + "/usr/zports/groups/base"
+    if dir.dir_exists(base_src)
+        println("  Copying base category...")
+        let cmd2 = "cp -r \"" + base_src + "\" \"" + base_dst + "\""
+        let pid2 = process.process_spawn_shell(cmd2)
+        if pid2 > 0
+            process.process_wait(pid2)
+        end if
+    end if
+
+    // Count copied ports
+    let core_count = count_ports(core_dst)
+    let base_count = count_ports(base_dst)
+    println("  Copied " + int_to_str(core_count) + " core ports")
+    println("  Copied " + int_to_str(base_count) + " base ports")
+
+    return true
+end copy_ports
+
+fn create_empty_ports(base: string): bool
+    // Create sample port directory
+    let sample_port = base + "/usr/zports/groups/core/hello"
+    let cmd = "mkdir -p \"" + sample_port + "\""
+    let pid = process.process_spawn_shell(cmd)
+    if pid > 0
+        process.process_wait(pid)
+    end if
+
+    // Create a simple Portfile
+    let portfile = sample_port + "/Portfile"
+    let content = "# Sample port for testing\n" +
+        "name = hello\n" +
+        "version = 1.0.0\n" +
+        "revision = 0\n" +
+        "description = Hello World test package\n" +
+        "homepage = https://example.com\n" +
+        "license = MIT\n" +
+        "\n" +
+        "# No sources - just creates a hello script\n" +
+        "sources = []\n" +
+        "\n" +
+        "[build]\n" +
+        "type = script\n" +
+        "\n" +
+        "[scripts]\n" +
+        "build = \"\"\"\n" +
+        "echo 'Building hello...'\n" +
+        "\"\"\"\n" +
+        "\n" +
+        "install = \"\"\"\n" +
+        "mkdir -p $DESTDIR/usr/bin\n" +
+        "echo '#!/bin/sh' > $DESTDIR/usr/bin/hello\n" +
+        "echo 'echo Hello from Coral!' >> $DESTDIR/usr/bin/hello\n" +
+        "chmod +x $DESTDIR/usr/bin/hello\n" +
+        "\"\"\"\n"
+
+    if not file.writeFile(portfile, content)
+        return false
+    end if
+
+    println("  Created sample port: core/hello")
+    return true
+end create_empty_ports
+
+fn count_ports(dir_path: string): int
+    if not dir.dir_exists(dir_path)
+        return 0
+    end if
+
+    mut entries: [string] = new [string](256)
+    let count = dir.list_dir(dir_path, entries, 256)
+
+    // Count only directories (each is a port)
+    mut ports = 0
+    mut i = 0
+    while i < count
+        let entry = entries[i]
+        if str.length(entry) > 0 and entry[0] != '.'
+            let full_path = path.join_path(dir_path, entry)
+            if dir.dir_exists(full_path)
+                ports = ports + 1
+            end if
+        end if
+        i = i + 1
+    end while
+
+    return ports
+end count_ports
+
+// Helper: convert int to string
+fn int_to_str(n: int): string
+    if n == 0
+        return "0"
+    end if
+
+    mut negative = false
+    mut value = n
+    if n < 0
+        negative = true
+        value = 0 - n
+    end if
+
+    mut result = ""
+    while value > 0
+        let digit = value % 10
+        result = str.concat(str.substring("0123456789", digit, 1), result)
+        value = value / 10
+    end while
+
+    if negative
+        result = str.concat("-", result)
+    end if
+
+    return result
+end int_to_str
+
+proc print_usage()
+    println("mkchroot - Create chrooted test environment for Coral")
+    println("")
+    println("Usage: mkchroot [options] [path]")
+    println("")
+    println("Options:")
+    println("  path                Chroot base path (default: /tmp/coral-chroot)")
+    println("  --coral-bin <path>  Copy coral binary into chroot at /usr/local/bin/coral")
+    println("  --help, -h          Show this help")
+    println("")
+    println("Description:")
+    println("  Creates a minimal filesystem structure suitable for testing")
+    println("  Coral package manager operations. Includes:")
+    println("")
+    println("    - Base system directories (/bin, /usr, /etc, /var, etc.)")
+    println("    - Coral configuration in /etc/coral/")
+    println("    - Package database directories in /var/lib/coral/")
+    println("    - Ports tree in /usr/zports/")
+    println("    - Sample ports copied from zygaena-ports repository")
+    println("    - Setup/teardown scripts for chroot mount management")
+    println("")
+    println("Generated Scripts (created alongside chroot):")
+    println("  <path>-setup.sh     Mount /dev, /proc, /sys, and host binaries")
+    println("  <path>-teardown.sh  Unmount all chroot mounts")
+    println("")
+    println("Examples:")
+    println("  # Create basic chroot")
+    println("  mkchroot")
+    println("  mkchroot /tmp/test-env")
+    println("")
+    println("  # Create chroot with coral binary")
+    println("  mkchroot --coral-bin ./coral /tmp/test-env")
+    println("")
+    println("  # Use --root flag (recommended, no mounts needed):")
+    println("  coral --root /tmp/coral-chroot db init")
+    println("  coral --root /tmp/coral-chroot install zlib")
+    println("  coral --root /tmp/coral-chroot list")
+    println("")
+    println("  # Or enter chroot directly:")
+    println("  sudo /tmp/coral-chroot-setup.sh /tmp/coral-chroot")
+    println("  sudo chroot /tmp/coral-chroot /bin/bash")
+    println("  coral db init")
+    println("  coral build core/zlib")
+    println("  exit")
+    println("  sudo /tmp/coral-chroot-teardown.sh /tmp/coral-chroot")
+end print_usage