=encoding utf8

=head1 NAME

std/zuzuzoo - Plan and install Zuzu distributions.

=head1 IMPLEMENTATION SUPPORT

This module is supported by zuzu.pl, zuzu-rust, and zuzu-js on Node and
Electron. It is not supported by zuzu-js in the browser.

=head1 DESCRIPTION

C<std/zuzuzoo> provides the package-management engine used by the
command-line C<zuzuzoo> tool. It supports installed distribution
metadata queries, source archive inspection, dependency-aware install
planning, distribution test execution, file installation, planned
target-root removal execution, installed metadata writing, standalone
safe removal planning and execution, installed-file verification,
latest-version checks, upgrade checks, and canonical pretty JSON
formatting.

The command-line C<zuzuzoo> wrapper delegates package behaviour to this
module and handles argument parsing, user prompts, output formatting,
JSON output selection, and exit-code translation.

=head1 EXPORTS

=head2 Functions

=over

=item C<< compare_versions(left, right) >>

Parameters: C<left> and C<right> are version strings. Returns:
C<Number>. Compares two versions, returning a negative, zero, or
positive value.

=item C<< list_installed(options?) >>, C<< query(module_name, options?) >>, C<< query_distribution(distribution_name, options?) >>

Parameters: names identify installed modules or distributions and
C<options> controls roots and output. Returns: value. Reads installed
distribution metadata.

=item C<< is_installed(module_name, min_version?, options?) >>, C<< installed_version(module_name, options?) >>

Parameters: C<module_name> identifies a module, C<min_version> is
optional, and C<options> controls roots. Returns: C<Boolean> or
C<String>/C<null>. Checks installed module state.

=item C<< pretty_json(value, options?) >>, C<< format_json(value, options?) >>

Parameters: C<value> is JSON-encodable data and C<options> controls
formatting. Returns: C<String>. Formats canonical JSON output.

=item C<< fetch_source(target, options?) >>, C<< load_distribution(target, options?) >>

Parameters: C<target> identifies a source archive or distribution and
C<options> controls fetch/load behaviour. Returns: value. Fetches or
loads distribution metadata.

=item C<< dependency_roots(options?) >>, C<< find_dependency(module_name, min_version?, options?) >>

Parameters: C<options> controls search roots and C<module_name> names a
dependency. Returns: value. Locates dependency sources.

=item C<< plan_install(targets, options?) >>, C<< plan_remove(targets, options?) >>

Parameters: C<targets> is an array of requested modules or
distributions. Returns: C<Dict>. Builds an install or removal plan.

=item C<< verify(targets, options?) >>, C<< latest(module_name, options?) >>, C<< can_upgrade(module_name, options?) >>

Parameters: C<targets> or C<module_name> identify installed or remote
items. Returns: value. Verifies installation state or checks available
versions.

=item C<< install(targets, options?) >>, C<< remove(targets, options?) >>

Parameters: C<targets> is an array of modules or distributions. Returns:
C<Dict>. Executes installation or removal.

=item C<< run_distribution_tests(install_action, options?) >>, C<< execute_removal(removal_action, options?) >>

Parameters: action dictionaries come from plans and C<options> controls
execution. Returns: C<Dict>. Runs tests or executes one removal action.

=item C<< format_install_plan(plan, options?) >>, C<< format_remove_plan(plan, options?) >>

Parameters: C<plan> is a plan dictionary. Returns: C<String>. Formats a
plan for display.

=back

=head2 Classes

=over

=item C<ZuzuzooLock>

Filesystem lock object.

=over

=item C<< lock.release() >>

Parameters: none. Returns: C<null>. Releases the lock.

=back

=item C<Zuzuzoo>

Stateful package-management helper. Its methods correspond to the
module-level functions and take the same parameters, without the final
C<options> argument where object configuration already supplies defaults.

=over

=item C<< zoo.config() >>

Parameters: none. Returns: C<Dict>. Returns the effective configuration.

=item C<< zoo.acquire_lock(operation, options?) >>

Parameters: C<operation> names the operation and C<options> controls
locking. Returns: C<ZuzuzooLock>. Acquires an operation lock.

=item C<< zoo.find_installed_module(module_name, options?) >>

Parameters: C<module_name> identifies a module and C<options> controls
roots. Returns: C<Dict> or C<null>. Finds installed metadata for a
module.

=item C<< zoo.find_installed_distribution(distribution_name, options?) >>

Parameters: C<distribution_name> identifies a distribution and
C<options> controls roots. Returns: C<Dict> or C<null>. Finds installed
metadata for a distribution.

=back

=back

=head1 COPYRIGHT AND LICENCE

B<< std/zuzuzoo >> is copyright Toby Inkster.

It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.

=cut

from std/data/json import JSON;
from std/archive import Archive;
from std/digest/sha import sha256_hex;
from std/io import Path, STDERR, STDOUT;
from std/net/http import UserAgent;
from std/proc import Env, Proc, sleep;
from std/string import contains, ends_with, join, split, starts_with, substr;
from std/time import Time;
from test/parser import parse as parse_tap;


function _opt ( options, key, fallback := null ) {
	if ( options instanceof Dict and options.exists(key) ) {
		return options.get(key);
	}
	return fallback;
}

function _progress ( options, message ) {
	if ( _opt( options, "progress", false ) ) {
		STDERR.say( "zuzuzoo: " _ message );
	}
	return true;
}

function _response_success ( response ) {
	return response.success() if response can "success";
	return true;
}

function _response_status_text ( response ) {
	let status := ( response can "status" ) ? response.status() : "?";
	let reason := ( response can "reason" ) ? response.reason() : "";
	return "" _ status _ " " _ reason;
}

function _archive_url_from_latest ( latest_info ) {
	let metadata := latest_info{remote_metadata};
	if (
		( metadata instanceof Dict or metadata instanceof PairList ) and
		metadata.exists("archive_url")
	) {
		return metadata{archive_url};
	}

	let remote_url := latest_info{remote_url};
	if ( ends_with( remote_url, ".json" ) ) {
		return substr( remote_url, 0, length remote_url - 5 ) _ ".tar.gz";
	}

	die(
		"Latest metadata URL does not identify a source archive " _
		"(remote_url=" _ remote_url _ ")"
	);
}

function _push_unique_string ( items, seen, value ) {
	let key := "" _ value;
	return false if key eq "";
	return false if seen.exists(key);
	seen.add( key, true );
	items.push(key);
	return true;
}

function _copy_options_with ( options, key, value ) {
	let out := {};
	if ( options instanceof Dict ) {
		for ( let existing_key in options.keys() ) {
			out.set( existing_key, options.get(existing_key) );
		}
	}
	out.set( key, value );
	return out;
}

function _is_windows_platform () {
	if ( "platform" in __system__ ) {
		let platform := lc( "" _ __system__.get("platform") );
		return platform eq "windows" or platform eq "mswin32";
	}
	return false;
}

function _join_dir ( String base, String child, Boolean windows ) {
	let sep := windows ? "\\": "/";
	return base _ sep _ child;
}

function _require_text ( obj, String key, String where ) {
	if ( not( obj instanceof Dict ) or not obj.exists(key) ) {
		die `Invalid installed metadata ${where}: missing ${key}`;
	}
	let value := obj.get(key);
	if ( not( value instanceof String ) or value eq "" ) {
		die `Invalid metadata ${where}: ${key} must be a string`;
	}
	return value;
}

function _require_object ( obj, String key, String where ) {
	if ( not( obj instanceof Dict ) or not obj.exists(key) ) {
		die `Invalid installed metadata ${where}: missing ${key}`;
	}
	let value := obj.get(key);
	if ( not( value instanceof Dict ) ) {
		die `Invalid metadata ${where}: ${key} must be an object`;
	}
	return value;
}

function _require_array ( obj, String key, String where ) {
	if ( not( obj instanceof Dict ) or not obj.exists(key) ) {
		die `Invalid installed metadata ${where}: missing ${key}`;
	}
	let value := obj.get(key);
	if ( not( value instanceof Array ) ) {
		die `Invalid metadata ${where}: ${key} must be an array`;
	}
	return value;
}

function _source_require_text ( obj, String key, String where ) {
	if ( not( obj instanceof Dict ) and not( obj instanceof PairList ) ) {
		die `Invalid source metadata ${where}: root must be an object`;
	}
	if ( not obj.exists(key) ) {
		die `Invalid source metadata ${where}: missing ${key}`;
	}
	let value := obj.get(key);
	if ( not( value instanceof String ) or value eq "" ) {
		die `Invalid source metadata ${where}: ${key} must be a string`;
	}
	return value;
}

function _validate_dependency_pair ( key, value, String where ) {
	if ( not( key instanceof String ) or key eq "" ) {
		die `Invalid source metadata ${where}: bad dependency name`;
	}
	if ( not( value instanceof String ) or value eq "" ) {
		die `Invalid source metadata ${where}: bad dependency version`;
	}
	return true;
}

function _validate_dependencies ( meta, String where ) {
	if ( not meta.exists("dependencies") ) {
		return true;
	}
	let deps := meta.get("dependencies");
	if ( not( deps instanceof Dict ) ) {
		die `Invalid metadata ${where}: dependencies must be an object`;
	}
	for ( let key in deps.keys() ) {
		if ( not( key instanceof String ) or key eq "" ) {
			die `Invalid metadata ${where}: bad dependency name`;
		}
		let value := deps.get(key);
		if ( not( value instanceof String ) or value eq "" ) {
			die `Invalid metadata ${where}: bad dependency version`;
		}
	}
	return true;
}

function _validate_source_dependencies ( meta, String where ) {
	if ( not meta.exists("dependencies") ) {
		return true;
	}
	let deps := meta.get("dependencies");
	if ( deps instanceof Dict ) {
		for ( let key in deps.keys() ) {
			_validate_dependency_pair( key, deps.get(key), where );
		}
		return true;
	}
	if ( deps instanceof PairList ) {
		let keys := deps.keys();
		let values := deps.values();
		let i := 0;
		while ( i < keys.length() ) {
			_validate_dependency_pair( keys[i], values[i], where );
			i++;
		}
		return true;
	}
	die `Invalid source metadata ${where}: dependencies must be an object`;
}

function _validate_source_metadata ( meta, String metadata_file ) {
	if ( not( meta instanceof Dict ) and not( meta instanceof PairList ) ) {
		die `Invalid source metadata ${metadata_file}: root must be an object`;
	}
	if ( meta.exists("installed") ) {
		die `Invalid source metadata ${metadata_file}: installed is not allowed`;
	}
	if ( meta.exists("modules") or meta.exists("scripts") ) {
		die(
			`Invalid source metadata ${metadata_file}: top-level ` _
			"modules/scripts are not allowed"
		);
	}

	_source_require_text( meta, "name", metadata_file );
	_source_require_text( meta, "version", metadata_file );
	_source_require_text( meta, "author", metadata_file );
	_source_require_text( meta, "license", metadata_file );

	if ( meta.exists("status") ) {
		let status := _source_require_text( meta, "status", metadata_file );
		if ( status ne "stable" and status ne "trial" ) {
			die `Invalid source metadata ${metadata_file}: bad status ${status}`;
		}
	}
	_validate_source_dependencies( meta, metadata_file );

	if ( meta instanceof Dict ) {
		meta.set( "metadata_file", metadata_file );
	}
	else {
		meta.add( "metadata_file", metadata_file );
	}
	return meta;
}

function _validate_entry ( entry, String kind, String where ) {
	if ( not( entry instanceof Dict ) ) {
		die `Invalid metadata ${where}: bad ${kind} entry`;
	}
	_require_text( entry, "source", where );
	_require_text( entry, "install_as", where );
	_require_text( entry, "sha256", where );
	if ( kind eq "script" and entry.exists("wrappers") ) {
		let wrappers := entry.get("wrappers");
		if ( not( wrappers instanceof Array ) ) {
			die `Invalid metadata ${where}: bad script wrappers`;
		}
		for ( let wrapper in wrappers ) {
			if ( not( wrapper instanceof String ) or wrapper eq "" ) {
				die `Invalid metadata ${where}: bad wrapper`;
			}
		}
	}
	return true;
}

function _validate_installed_metadata ( meta, String metadata_file ) {
	if ( not( meta instanceof Dict ) ) {
		die `Invalid metadata ${metadata_file}: root must be an object`;
	}
	if ( meta.exists("modules") or meta.exists("scripts") ) {
		die(
			`Invalid metadata ${metadata_file}: top-level ` _
			"modules/scripts are not allowed"
		);
	}

	_require_text( meta, "name", metadata_file );
	_require_text( meta, "version", metadata_file );
	_require_text( meta, "author", metadata_file );
	_require_text( meta, "license", metadata_file );
	let status := _require_text( meta, "status", metadata_file );
	if ( status ne "stable" and status ne "trial" ) {
		die `Invalid metadata ${metadata_file}: bad status ${status}`;
	}
	_validate_dependencies( meta, metadata_file );

	let installed := _require_object( meta, "installed", metadata_file );
	let zdf := _require_text( installed, "zdf", metadata_file );
	if ( zdf ne "ZDF-1" ) {
		die `Invalid metadata ${metadata_file}: bad installed.zdf`;
	}
	_require_text( installed, "lib_dir", metadata_file );
	_require_text( installed, "bin_dir", metadata_file );
	_require_text( installed, "meta_dir", metadata_file );

	let modules := _require_array( installed, "modules", metadata_file );
	let scripts := _require_array( installed, "scripts", metadata_file );
	if ( modules.length() + scripts.length() = 0 ) {
		die `Invalid metadata ${metadata_file}: no installed files`;
	}

	for ( let module in modules ) {
		_validate_entry( module, "module", metadata_file );
	}
	for ( let script in scripts ) {
		_validate_entry( script, "script", metadata_file );
	}

	meta.set( "metadata_file", metadata_file );
	return meta;
}

function _parse_version ( version ) {
	let text := "" _ version;
	let nums := [];
	let i := 0;
	let n := length text;

	while ( i < n and substr( text, i, 1 ) ~ /^[0-9]$/ ) {
		let start := i;
		while ( i < n and substr( text, i, 1 ) ~ /^[0-9]$/ ) {
			i++;
		}
		nums.push( int( substr( text, start, i - start ) ) );

		if (
			i < n - 1 and
			substr( text, i, 1 ) eq "." and
			substr( text, i + 1, 1 ) ~ /^[0-9]$/
		) {
			i++;
			next;
		}
		last;
	}

	return {
		nums: nums,
		suffix: substr( text, i ),
	};
}

function compare_versions ( left, right ) {
	let a := _parse_version(left);
	let b := _parse_version(right);
	let max := a{nums}.length() > b{nums}.length()
		? a{nums}.length()
		: b{nums}.length();

	let i := 0;
	while ( i < max ) {
		let av := i < a{nums}.length() ? a{nums}[i]: 0;
		let bv := i < b{nums}.length() ? b{nums}[i]: 0;
		return av <=> bv if av != bv;
		i++;
	}

	if ( a{suffix} eq "" and b{suffix} ne "" ) {
		return 1;
	}
	if ( a{suffix} ne "" and b{suffix} eq "" ) {
		return -1;
	}
	return a{suffix} cmp b{suffix};
}

function _module_key ( module_name ) {
	let text := "" _ module_name;
	if ( ends_with( text, ".zzm" ) ) {
		return substr( text, 0, length text - 4 );
	}
	return text;
}

function _is_url ( String target ) {
	return (
		starts_with( target, "http://" ) or
		starts_with( target, "https://" )
	);
}

function _trim_right_slash ( String text ) {
	let out := text;
	while ( length out > 0 and substr( out, length out - 1, 1 ) eq "/" ) {
		out := substr( out, 0, length out - 1 );
	}
	return out;
}

function _safe_archive_root ( archive, String where ) {
	if ( not( archive instanceof Dict ) or not( archive.get("entries") instanceof Array ) ) {
		die `Invalid archive ${where}: entries must be an array`;
	}

	let seen := {};
	let root := null;
	for ( let entry in archive{entries} ) {
		if ( not( entry instanceof Dict ) or not( entry.get("path") instanceof String ) ) {
			die `Invalid archive ${where}: bad entry path`;
		}
		let path := entry{path};
		if ( path eq "" ) {
			die `Invalid archive ${where}: empty path`;
		}
		if ( starts_with( path, "/" ) or contains( path, "\\" ) ) {
			die `Invalid archive ${where}: unsafe path ${path}`;
		}
		if ( seen.exists(path) ) {
			die `Invalid archive ${where}: duplicate path ${path}`;
		}
		seen.add( path, true );

		let parts := split( path, "/" );
		if ( parts.length() == 0 or parts[0] eq "" ) {
			die `Invalid archive ${where}: empty path component`;
		}
		for ( let part in parts ) {
			if ( part eq "" or part eq "." or part eq ".." ) {
				die `Invalid archive ${where}: unsafe path ${path}`;
			}
		}
		if ( root instanceof Null ) {
			root := parts[0];
		}
		else if ( root ne parts[0] ) {
			die `Invalid archive ${where}: multiple top-level roots`;
		}
	}

	die `Invalid archive ${where}: no entries` if root instanceof Null;
	return root;
}

function _ensure_dir ( path ) {
	return true if path.exists();
	let parent := path.parent();
	_ensure_dir(parent) if not parent.exists();
	path.mkdir();
	return true;
}

function _mkdir_parent ( path ) {
	_ensure_dir( path.parent() );
	return true;
}

function _extract_archive ( archive, String root_name, root_dir ) {
	for ( let entry in archive{entries} ) {
		let parts := split( entry{path}, "/" );
		let out := root_dir;
		let i := 1;
		while ( i < parts.length() ) {
			out := out.child(parts[i]);
			i++;
		}
		_mkdir_parent(out);
		out.spew(entry{data});
	}
	return true;
}

function _relative_path ( root, path ) {
	let prefix := root.to_String() _ "/";
	let text := path.to_String();
	if ( starts_with( text, prefix ) ) {
		return substr( text, length prefix );
	}
	return path.basename();
}

function _discover_files ( root, dir_name, extension ) {
	let base := root.child(dir_name);
	return [] if not base.exists();
	return [] if not base.is_dir();

	let found := [];
	function walk ( path ) {
		for ( let child in path.children() ) {
			if ( child.is_dir() ) {
				walk(child);
			}
			else if ( child.is_file() and ends_with( child.basename(), extension ) ) {
				found.push( _relative_path( root, child ) );
			}
		}
	}
	walk(base);
	return found.sort( fn ( a, b ) -> a cmp b );
}

function _has_extension ( String basename ) {
	return contains( basename, "." );
}

function _has_zuzu_shebang ( path ) {
	let text := "";
	try {
		text := path.slurp_utf8();
	}
	catch {
		return false;
	}

	let lines := split( text, "\n" );
	let first_line := lines.length() == 0 ? "" : lines[0];
	return starts_with( first_line, "#!" ) and contains( first_line, "zuzu" );
}

function _discover_scripts ( root ) {
	let base := root.child("scripts");
	return [] if not base.exists();
	return [] if not base.is_dir();

	let found := [];
	function walk ( path ) {
		for ( let child in path.children() ) {
			if ( child.is_dir() ) {
				walk(child);
			}
			else if ( child.is_file() ) {
				let basename := child.basename();
				if (
					ends_with( basename, ".zzs" ) or
					(
						not _has_extension(basename) and
						_has_zuzu_shebang(child)
					)
				) {
					found.push( _relative_path( root, child ) );
				}
			}
		}
	}
	walk(base);
	return found.sort( fn ( a, b ) -> a cmp b );
}

function _module_install_name ( String source ) {
	return substr( source, length "modules/" );
}

function _script_install_name ( String source ) {
	return substr( source, length "scripts/" );
}

function _target_list ( targets ) {
	return targets instanceof Array ? targets : [ targets ];
}

function _root_key ( root ) {
	return (
		root{lib_dir} _ "\n" _
		root{bin_dir} _ "\n" _
		root{meta_dir}
	);
}

function _root_from_config ( String name, String kind, cfg ) {
	return {
		name: name,
		kind: kind,
		lib_dir: cfg{lib_dir},
		bin_dir: cfg{bin_dir},
		meta_dir: cfg{meta_dir},
		global: cfg{global},
		windows: cfg{windows},
	};
}

function _require_root_text ( root, String key, String where ) {
	if ( not( root instanceof Dict ) or not root.exists(key) ) {
		die `Invalid dependency root ${where}: missing ${key}`;
	}
	let value := root.get(key);
	if ( not( value instanceof String ) or value eq "" ) {
		die `Invalid dependency root ${where}: ${key} must be a string`;
	}
	return value;
}

function _custom_root ( root, String where ) {
	return {
		name: _require_root_text( root, "name", where ),
		kind: "custom",
		lib_dir: _require_root_text( root, "lib_dir", where ),
		bin_dir: _require_root_text( root, "bin_dir", where ),
		meta_dir: _require_root_text( root, "meta_dir", where ),
		global: root.exists("global") ? root{global} : false,
		windows: root.exists("windows") ? root{windows} : false,
	};
}

function _root_override ( root, String name, String kind, String where ) {
	return {
		name: name,
		kind: kind,
		lib_dir: _require_root_text( root, "lib_dir", where ),
		bin_dir: _require_root_text( root, "bin_dir", where ),
		meta_dir: _require_root_text( root, "meta_dir", where ),
		global: root.exists("global") ? root{global} : false,
		windows: root.exists("windows") ? root{windows} : false,
	};
}

function _add_root ( roots, seen, root ) {
	let key := _root_key(root);
	return false if seen.exists(key);
	seen.add( key, true );
	roots.push(root);
	return true;
}

function _path_join ( String base, String child, Boolean windows ) {
	let sep := windows ? "\\": "/";
	return base _ sep _ child;
}

function _path_child ( String base, String relative ) {
	let out := new Path(base);
	for ( let part in split( relative, "/" ) ) {
		out := out.child(part) if part ne "";
	}
	return out;
}

function _relative_basename ( String relative ) {
	let parts := split( relative, "/" );
	return parts[ parts.length() - 1 ];
}

function _replace_script_suffix ( String relative, String suffix ) {
	if ( ends_with( relative, ".zzs" ) ) {
		return substr( relative, 0, length relative - 4 ) _ suffix;
	}
	return relative _ suffix;
}

function _installed_at () {
	return ( new Time() ).strftime("%Y-%m-%dT%H:%M:%SZ");
}

function _now_epoch () {
	return ( new Time() ).epoch();
}

function _temp_parent ( options? ) {
	let temp_root := _opt( options, "temp_root", null );
	if ( temp_root instanceof Null ) {
		return null;
	}
	let root := new Path(temp_root);
	_ensure_dir(root);
	return root;
}

function _unique_temp_path ( parent, String prefix, String suffix ) {
	let base := prefix _ Proc.pid() _ "-" _ _now_epoch();
	let i := 0;
	while ( true ) {
		let candidate := parent.child( base _ "-" _ i _ suffix );
		return candidate if not candidate.exists();
		i++;
	}
}

function _new_temp_dir ( options?, String prefix := "zuzuzoo-" ) {
	let parent := _temp_parent(options);
	if ( parent instanceof Null ) {
		return Path.tempdir();
	}

	let i := 0;
	while ( true ) {
		let candidate := _unique_temp_path(
			parent,
			prefix,
			".d",
		);
		if ( candidate.mkdir_exclusive() ) {
			return candidate;
		}
		i++;
		die `Could not allocate temporary directory in ${parent}`
			if i > 1000;
	}
}

function _cleanup_path ( path ) {
	return false if path instanceof Null;
	try {
		if ( path.exists() ) {
			if ( path.is_dir() ) {
				path.remove_tree();
			}
			else {
				path.remove();
			}
			return true;
		}
	}
	catch ( Exception e ) {
		return false;
	}
	return false;
}

function _cleanup_source ( source, options? ) {
	return false if source instanceof Null;
	return false if _opt( options, "keep_work_dirs", false );
	if ( source.exists("temp_dir") ) {
		return _cleanup_path(source{temp_dir});
	}
	return false;
}

function _cleanup_install_action ( install_action, options? ) {
	return false if _opt( options, "keep_work_dirs", false );
	_cleanup_source( install_action{source}, options )
		if install_action.exists("source");
	_cleanup_path( install_action{work_dir_obj} )
		if install_action.exists("work_dir_obj");
	return true;
}

function _cleanup_plan_work_dirs ( plan, options? ) {
	return false if plan instanceof Null;
	return false if _opt( options, "keep_work_dirs", false );
	for ( let install_action in plan.get( "installs", [] ) ) {
		_cleanup_install_action( install_action, options );
	}
	return true;
}

function _cleanup_loaded_work_dirs ( loaded, options? ) {
	return false if _opt( options, "keep_work_dirs", false );
	for ( let item in loaded ) {
		_cleanup_install_action( item, options );
	}
	return true;
}

function _atomic_temp_sibling ( path, String suffix := ".tmp" ) {
	return _unique_temp_path(
		path.parent(),
		"." _ path.basename() _ ".",
		suffix,
	);
}

function _atomic_json_write ( path, value ) {
	_mkdir_parent(path);
	let temp := _atomic_temp_sibling(path);
	let codec := new JSON( pretty: true, canonical: true );
	try {
		codec.dump( temp, value );
		codec.load(temp);
		temp.move(path);
	}
	catch ( Exception e ) {
		_cleanup_path(temp);
		throw e;
	}
	return path;
}

function _copy_file_atomic ( source, destination, chmod_mode? ) {
	_mkdir_parent(destination);
	let temp := _atomic_temp_sibling(destination);
	try {
		source.copy(temp);
		let temp_sha := sha256_hex(temp.slurp());
		temp.chmod(chmod_mode) if not( chmod_mode instanceof Null );
		temp.move(destination);
		let final_bytes := destination.slurp();
		let final_sha := sha256_hex(final_bytes);
		if ( final_sha ne temp_sha ) {
			die(
				`Atomic install verification failed for ${destination}: ` _
				`expected ${temp_sha}, got ${final_sha}`
			);
		}
		return {
			sha256: final_sha,
			size: destination.size(),
		};
	}
	catch ( Exception e ) {
		_cleanup_path(temp);
		throw e;
	}
}

function _spew_utf8_atomic ( destination, String text ) {
	_mkdir_parent(destination);
	let temp := _atomic_temp_sibling(destination);
	try {
		temp.spew_utf8(text);
		temp.move(destination);
	}
	catch ( Exception e ) {
		_cleanup_path(temp);
		throw e;
	}
	return destination;
}

function _source_context ( source ) {
	let parts := [
		"target=" _ source{value},
		"source_type=" _ source{type},
	];
	parts.push( "url=" _ source{url} ) if source.exists("url");
	parts.push( "resolved_url=" _ source{resolved_url} )
		if source.exists("resolved_url");
	parts.push( "path=" _ source{path} ) if source.exists("path");
	return join( ", ", parts );
}

function _corrupt_archive_error ( source, underlying ) {
	return (
		"Corrupt source archive (" _ _source_context(source) _
		"): " _ underlying.to_String()
	);
}

function _cache_key ( String url ) {
	return sha256_hex(to_binary(url));
}

function _cache_paths ( String cache_dir, String url ) {
	let dir := new Path(cache_dir);
	_ensure_dir(dir);
	let key := _cache_key(url);
	return {
		dir: dir,
		key: key,
		archive: dir.child(key _ ".archive"),
		sidecar: dir.child(key _ ".json"),
	};
}

function _delete_cache_entry ( paths ) {
	_cleanup_path(paths{archive});
	_cleanup_path(paths{sidecar});
	return true;
}

function _validate_cache_entry ( paths ) {
	return false if not paths{archive}.exists();
	return false if not paths{sidecar}.exists();

	let sidecar := null;
	try {
		sidecar := ( new JSON() ).load(paths{sidecar});
	}
	catch ( Exception e ) {
		return false;
	}

	let bytes := paths{archive}.slurp();
	let actual_sha := sha256_hex(bytes);
	if ( sidecar.get( "archive_sha256", "" ) ne actual_sha ) {
		return false;
	}
	if ( sidecar.get( "byte_size", -1 ) != paths{archive}.size() ) {
		return false;
	}
	try {
		Archive.decode(bytes);
	}
	catch ( Exception e ) {
		return false;
	}
	return sidecar;
}

function _write_cache_entry ( paths, source, temp_path ) {
	let bytes := temp_path.slurp();
	let sidecar := {
		original_url: source{url},
		resolved_url: source{resolved_url},
		downloaded_at: _installed_at(),
		archive_sha256: sha256_hex(bytes),
		byte_size: temp_path.size(),
	};
	Archive.decode(bytes);
	let archive_temp := _atomic_temp_sibling(paths{archive});
	let sidecar_temp := _atomic_temp_sibling(paths{sidecar});
	try {
		temp_path.copy(archive_temp);
		archive_temp.move(paths{archive});
		( new JSON( pretty: true, canonical: true ) ).dump(
			sidecar_temp,
			sidecar,
		);
		( new JSON() ).load(sidecar_temp);
		sidecar_temp.move(paths{sidecar});
	}
	catch ( Exception e ) {
		_cleanup_path(archive_temp);
		_cleanup_path(sidecar_temp);
		_delete_cache_entry(paths);
		throw e;
	}
	return sidecar;
}

function _check_expected_source_sha ( source, expected_sha256 ) {
	return true if expected_sha256 instanceof Null;
	let actual := sha256_hex( ( new Path(source{path}) ).slurp() );
	if ( actual ne expected_sha256 ) {
		die(
			"Source checksum mismatch (" _ _source_context(source) _
			`): expected ${expected_sha256}, got ${actual}`
		);
	}
	return true;
}

function _dependency_chain ( stack, dependency_of, module_name ) {
	let chain := [];
	for ( let item in stack ) {
		chain.push(item);
	}
	if ( not( dependency_of instanceof Null ) ) {
		chain.push(dependency_of{metadata}{name});
	}
	chain.push(module_name);
	return join( " -> ", chain );
}

function _dependency_conflict_message (
	dep,
	dependency_of,
	stack,
	planned_action,
	conflicting_dist
) {
	let requested_by := dependency_of instanceof Null
		? "<requested target>"
		: dependency_of{metadata}{name};
	let planned_text := planned_action instanceof Null
		? "none"
		: planned_action{metadata}{name} _ " " _
			planned_action{metadata}{version};
	let conflicting_text := conflicting_dist{metadata}{name} _ " " _
		conflicting_dist{metadata}{version};
	return (
		"Dependency conflict: requested dependency " _
		dep{module_name} _ " >= " _ dep{min_version} _
		"; requester chain " _
		_dependency_chain(
			stack,
			dependency_of,
			dep{module_name},
		) _
		"; requested by " _ requested_by _
		"; planned/provided version " _ planned_text _
		"; conflicting planned/provided version " _
		conflicting_text
	);
}

function _planned_version_conflict_message (
	String dist_name,
	existing,
	dist,
	String target_text
) {
	return (
		`Conflicting planned versions for ${dist_name}: ` _
		existing{metadata}{version} _ " and " _
		dist{metadata}{version} _
		"; existing target " _ existing{target} _
		"; conflicting target " _ target_text _
		"; existing metadata " _
		existing{metadata}{metadata_file} _
		"; conflicting metadata " _
		dist{metadata}{metadata_file}
	);
}

function _copy_dependencies ( metadata ) {
	let out := {};
	return out if not metadata.exists("dependencies");
	let deps := metadata{dependencies};
	if ( deps instanceof PairList ) {
		let keys := deps.keys();
		let values := deps.values();
		let i := 0;
		while ( i < keys.length() ) {
			out.set( keys[i], values[i] );
			i++;
		}
		return out;
	}
	for ( let key in deps.keys() ) {
		out.set( key, deps.get(key) );
	}
	return out;
}

function _copy_source_record ( source ) {
	let out := {
		type: source{type},
		value: source{value},
	};
	out{url} := source{url} if source.exists("url");
	out{resolved_url} := source{resolved_url}
		if source.exists("resolved_url");
	out{path} := source{path} if source.exists("path");
	return out;
}

function _copy_source_metadata ( metadata ) {
	let out := {};
	for ( let key in metadata.keys() ) {
		next if key eq "metadata_file";
		if ( key eq "dependencies" ) {
			out.set( key, _copy_dependencies(metadata) );
		}
		else {
			out.set( key, metadata.get(key) );
		}
	}
	out.set( "status", "stable" ) if not out.exists("status");
	out.set( "dependencies", {} ) if not out.exists("dependencies");
	return out;
}

function _test_ok ( parsed, run_result ) {
	return false if not Proc.is_success(run_result);
	return false if parsed{planned} instanceof Null;
	return false if parsed{assertions}{failed} > 0;
	return true;
}

function _dependency_entries ( metadata ) {
	let out := [];
	return out if not metadata.exists("dependencies");

	let deps := metadata{dependencies};
	if ( deps instanceof PairList ) {
		let keys := deps.keys();
		let values := deps.values();
		let i := 0;
		while ( i < keys.length() ) {
			out.push(
				{
					module_name: keys[i],
					min_version: values[i],
				},
			);
			i++;
		}
		return out;
	}

	let keys := deps.keys();
	for ( let key in keys ) {
		out.push(
			{
				module_name: key,
				min_version: deps.get(key),
			},
		);
	}
	return out;
}

function _version_satisfies ( version, min_version ) {
	return true if min_version instanceof Null;
	return true if "" _ min_version eq "0";
	return compare_versions( version, min_version ) >= 0;
}

function _provides_module ( dist, module_name, min_version ) {
	return false if not _version_satisfies( dist{version}, min_version );
	let wanted := _module_key(module_name);
	for ( let module in dist{installed}{modules} ) {
		return true if _module_key( module{install_as} ) eq wanted;
	}
	return false;
}

function _planned_provides_module ( install_action, module_name, min_version ) {
	return false if not _version_satisfies(
		install_action{metadata}{version},
		min_version,
	);
	let wanted := _module_key(module_name);
	for ( let module in install_action{modules} ) {
		return true if _module_key( module{install_as} ) eq wanted;
	}
	return false;
}

function _loaded_distribution_provides ( dist, module_name, min_version ) {
	return false if not _version_satisfies(
		dist{metadata}{version},
		min_version,
	);
	let wanted := _module_key(module_name);
	for ( let module in dist{modules} ) {
		return true if _module_key( module{install_as} ) eq wanted;
	}
	return false;
}

function _metadata_files_in_dir ( String meta_dir ) {
	let dir := new Path(meta_dir);
	return [] if not dir.exists();
	die `Metadata path is not a directory: ${dir}`
		if not dir.is_dir();

	let files := [];
	for ( let child in dir.children() ) {
		if (
			child.is_file() and
			ends_with( child.basename(), ".json" )
		) {
			files.push(child);
		}
	}
	return files.sort(
		fn ( a, b ) -> a.to_String() cmp b.to_String()
	);
}

function _list_installed_in_root ( root ) {
	let codec := new JSON();
	let installed := [];
	for ( let file in _metadata_files_in_dir( root{meta_dir} ) ) {
		let data := codec.load(file);
		let valid := _validate_installed_metadata(
			data,
			file.to_String(),
		);
		installed.push(valid);
	}

	return installed.sort( function ( a, b ) {
		let name_cmp := a{name} cmp b{name};
		return name_cmp if name_cmp != 0;
		return compare_versions( b{version}, a{version} );
	} );
}

function _candidate_record (
	String source,
	module_name,
	min_version,
	version,
	distribution,
	root,
	metadata_file,
	install_action
) {
	let record := {
		module_name: "" _ module_name,
		min_version: min_version instanceof Null ? null : "" _ min_version,
		version: "" _ version,
		source: source,
		distribution: distribution,
		metadata_file: metadata_file,
	};
	record{root} := root if not( root instanceof Null );
	record{install} := install_action if not( install_action instanceof Null );
	return record;
}

function _better_dependency_candidate ( current, candidate, root_order ) {
	return true if current instanceof Null;

	let version_cmp := compare_versions(
		candidate{version},
		current{version},
	);
	return true if version_cmp > 0;
	return false if version_cmp < 0;

	let current_order := root_order.exists(current{root}{name})
		? root_order.get(current{root}{name})
		: 1000000;
	let candidate_order := root_order.exists(candidate{root}{name})
		? root_order.get(candidate{root}{name})
		: 1000000;
	return true if candidate_order < current_order;
	return false if candidate_order > current_order;

	return candidate{metadata_file} lt current{metadata_file};
}

function _find_dependency_in_roots ( roots, module_name, min_version ) {
	let root_order := {};
	let i := 0;
	while ( i < roots.length() ) {
		root_order.add( roots[i]{name}, i );
		i++;
	}

	let best := null;
	for ( let root in roots ) {
		for ( let dist in _list_installed_in_root(root) ) {
			if ( _provides_module( dist, module_name, min_version ) ) {
				let candidate := _candidate_record(
					"root",
					module_name,
					min_version,
					dist{version},
					dist{name},
					root,
					dist{metadata_file},
					null,
				);
				candidate{installed} := dist;
				if (
					_better_dependency_candidate(
						best,
						candidate,
						root_order,
					)
				) {
					best := candidate;
				}
			}
		}
	}
	return best;
}

function _find_dependency_in_planned ( planned, module_name, min_version ) {
	let best := null;
	for ( let install_action in planned ) {
		if ( _planned_provides_module( install_action, module_name, min_version ) ) {
			let candidate := _candidate_record(
				"planned",
				module_name,
				min_version,
				install_action{metadata}{version},
				install_action{metadata}{name},
				install_action{target_root},
				install_action{metadata}{metadata_file},
				install_action,
			);
			if (
					best instanceof Null or
				compare_versions(
					candidate{version},
					best{version},
				) > 0 or
				(
					compare_versions(
						candidate{version},
						best{version},
					) == 0 and
					candidate{metadata_file} lt best{metadata_file}
				)
			) {
				best := candidate;
			}
		}
	}
	return best;
}

function _runtime_module_path ( module_name ) {
	return null if not starts_with( "" _ module_name, "std/" );

	let inc := [];
	if ( "inc" in __system__ ) {
		inc := __system__.get("inc");
	}
	return null if inc ≡ null;
	if ( not( inc instanceof Array ) ) {
		let separator := _is_windows_platform() ? ";" : ":";
		inc := split( "" _ inc, separator );
	}

	let relative := module_name _ ".zzm";
	for ( let root in inc ) {
		next if root eq "";
		let path := ( new Path(root) ).child(relative);
		return path if path.exists() and path.is_file();
	}
	return null;
}

function _runtime_include_dirs () {
	let out := [];
	let seen := {};
	let inc := [];
	if ( "inc" in __system__ ) {
		inc := __system__.get("inc");
	}
	return out if inc ≡ null;
	if ( not( inc instanceof Array ) ) {
		let separator := _is_windows_platform() ? ";" : ":";
		inc := split( "" _ inc, separator );
	}

	for ( let root in inc ) {
		next if root eq "";
		let path := ( new Path(root) ).absolute();
		_push_unique_string( out, seen, path.to_String() )
			if path.exists() and path.is_dir();
	}
	return out;
}

function _find_dependency_in_runtime ( module_name, min_version ) {
	return null if not _version_satisfies( "0", min_version );

	let module_path := _runtime_module_path(module_name);
	return null if module_path instanceof Null;

	let root := {
		name: "runtime",
		lib_dir: "",
		bin_dir: "",
		meta_dir: "",
	};
	let candidate := _candidate_record(
		"runtime",
		module_name,
		min_version,
		"0",
		"zuzu-runtime",
		root,
		module_path.to_String(),
		null,
	);
	candidate{module_file} := module_path.to_String();
	return candidate;
}

function _find_dependency_for_plan (
	planned,
	roots,
	module_name,
	min_version
) {
	let found := _find_dependency_in_planned(
		planned,
		module_name,
		min_version,
	);
	return found if not( found instanceof Null );
	found := _find_dependency_in_roots( roots, module_name, min_version );
	return found if not( found instanceof Null );
	return _find_dependency_in_runtime( module_name, min_version );
}

function _cycle_path ( stack, module_name ) {
	let path := [];
	let started := false;
	for ( let item in stack ) {
		started := true if item eq module_name;
		path.push(item) if started;
	}
	path.push(module_name);
	return join( " -> ", path );
}

function _stack_contains ( stack, value ) {
	for ( let item in stack ) {
		return true if item eq value;
	}
	return false;
}

function _remove_file_kind_order ( String kind ) {
	return kind eq "metadata" ? 1 : 0;
}

function _remove_file_cmp ( a, b ) {
	let kind_cmp := _remove_file_kind_order(a{kind}) <=>
		_remove_file_kind_order(b{kind});
	return kind_cmp if kind_cmp != 0;
	let path_cmp := a{path} cmp b{path};
	return path_cmp if path_cmp != 0;
	return a{kind} cmp b{kind};
}

function _removal_cmp ( a, b ) {
	let name_cmp := a{name} cmp b{name};
	return name_cmp if name_cmp != 0;
	let version_cmp := compare_versions( b{version}, a{version} );
	return version_cmp if version_cmp != 0;
	return a{metadata_file} cmp b{metadata_file};
}

function _owner_record ( dist ) {
	return {
		name: dist{name},
		version: dist{version},
		metadata_file: dist{metadata_file},
	};
}

function _same_owner ( left, right ) {
	return left{metadata_file} eq right{metadata_file};
}

function _owner_list_contains ( owners, owner ) {
	for ( let item in owners ) {
		return true if _same_owner( item, owner );
	}
	return false;
}

function _add_owner_to_list ( owners, owner ) {
	return false if _owner_list_contains( owners, owner );
	owners.push(owner);
	return true;
}

function _add_owner_for_path ( by_path, String path, owner ) {
	if ( not by_path.exists(path) ) {
		by_path.add( path, [] );
	}
	_add_owner_to_list( by_path.get(path), owner );
	return true;
}

function _remove_target_record ( target, options? ) {
	if ( target instanceof Dict ) {
		let type := target.exists("type") ? "" _ target{type} : "";
		let value := target.exists("value") ? "" _ target{value} : "";
		return {
			type: type,
			value: value,
			raw: target,
		};
	}
	return {
		type: _opt( options, "dist", false ) ? "distribution" : "module",
		value: "" _ target,
		raw: target,
	};
}

function _latest_module_name ( module_name ) {
	if ( not( module_name instanceof String ) ) {
		die "latest target must be a module name";
	}
	if ( module_name eq "" or _is_url(module_name) ) {
		die "latest target must be a module name";
	}
	let path := new Path(module_name);
	if ( path.exists() ) {
		die "latest target must be a module name";
	}
	return module_name;
}

function _latest_status ( installed_version, remote_version ) {
	return "not-installed" if installed_version instanceof Null;
	let comparison := compare_versions( installed_version, remote_version );
	return "current" if comparison == 0;
	return comparison < 0 ? "outdated" : "newer-local";
}

function _add_verify_target_error ( result, code, target, message ) {
	result{errors}.push(
		{
			code: code,
			target: target,
			message: message,
		},
	);
	return true;
}

class ZuzuzooLock {
	let lock_path := null;
	let owner_file := null;
	let acquired := false;

	method release () {
		return false if not acquired;
		_cleanup_path(owner_file);
		_cleanup_path(lock_path);
		acquired := false;
		return true;
	}
}

class Zuzuzoo {
	let lib_dir := null;
	let bin_dir := null;
	let meta_dir := null;
	let global := false;
	let windows := null;
	let home := null;
	let userprofile := null;
	let base_url := "https://zuzulang.org";
	let user_agent := null;
	let zuzu_command := "zuzu";
	let dependency_roots := [];
	let global_root := null;

	method _user_agent () {
		return not( user_agent instanceof Null ) ? user_agent : new UserAgent();
	}

	method config () {
		let resolved_windows := windows instanceof Null
			? _is_windows_platform()
			: windows;
		let resolved_global := global ? true: false;

		if ( resolved_windows and resolved_global ) {
			die "Global Windows installs are not supported";
		}

		let base_home := not( home instanceof Null ) ? home : Env.get( "HOME", "" );
		let base_userprofile := not( userprofile instanceof Null )
			? userprofile
			: Env.get( "USERPROFILE", "" );

		let resolved_lib := lib_dir;
		let resolved_bin := bin_dir;
		let resolved_meta := meta_dir;
		let needs_default := (
			resolved_lib instanceof Null or
			resolved_bin instanceof Null or
			resolved_meta instanceof Null
		);

		if ( resolved_windows ) {
			if ( needs_default ) {
				die "USERPROFILE required for Windows install"
					if base_userprofile eq "";
				let root := _join_dir(
					base_userprofile,
					".zuzu",
					true,
				);
				resolved_lib := _join_dir(
					root,
					"modules",
					true,
				)
					if resolved_lib instanceof Null;
				resolved_bin := _join_dir( root, "bin", true )
					if resolved_bin instanceof Null;
				resolved_meta := _join_dir( root, "meta", true )
					if resolved_meta instanceof Null;
			}
		}
		else if ( resolved_global ) {
			resolved_lib := "/var/lib/zuzu/modules"
				if resolved_lib instanceof Null;
			resolved_bin := "/usr/local/bin"
				if resolved_bin instanceof Null;
			resolved_meta := "/var/lib/zuzu/meta"
				if resolved_meta instanceof Null;
		}
		else {
			if ( needs_default ) {
				die "HOME required for POSIX user install"
					if base_home eq "";
				let root := _join_dir(
					base_home,
					".zuzu",
					false,
				);
				resolved_lib := _join_dir(
					root,
					"modules",
					false,
				)
					if resolved_lib instanceof Null;
				resolved_bin := _join_dir( root, "bin", false )
					if resolved_bin instanceof Null;
				resolved_meta := _join_dir(
					root,
					"meta",
					false,
				)
					if resolved_meta instanceof Null;
			}
		}

		return {
			lib_dir: resolved_lib,
			bin_dir: resolved_bin,
			meta_dir: resolved_meta,
			global: resolved_global,
			windows: resolved_windows,
		};
	}

	method acquire_lock ( operation, options? ) {
		if ( not _opt( options, "lock", true ) ) {
			return new ZuzuzooLock( acquired: false );
		}

		let cfg := self.config();
		let meta_path := new Path(cfg{meta_dir});
		_ensure_dir(meta_path);
		let lock_path := meta_path.child(".zuzuzoo.lock");
		let owner_file := lock_path.child("owner.json");
		let timeout := _opt( options, "lock_timeout", 30 );
		let poll := _opt( options, "lock_poll", 0.1 );
		let started := _now_epoch();

		while ( true ) {
			if ( lock_path.mkdir_exclusive() ) {
				let owner := {
					pid: Proc.pid(),
					created_at: _installed_at(),
					meta_dir: cfg{meta_dir},
					operation: "" _ operation,
				};
				try {
					( new JSON( pretty: true, canonical: true ) ).dump(
						owner_file,
						owner,
					);
				}
				catch ( Exception e ) {
					_cleanup_path(lock_path);
					throw e;
				}
				return new ZuzuzooLock(
					lock_path: lock_path,
					owner_file: owner_file,
					acquired: true,
				);
			}

			if ( _now_epoch() - started >= timeout ) {
				let owner_text := "";
				try {
					owner_text := owner_file.slurp_utf8();
				}
				catch ( Exception e ) {
					owner_text := "<unreadable>";
				}
				die(
					`Timed out waiting for Zuzuzoo lock ${lock_path} ` _
					`after ${timeout} seconds; owner=${owner_text}`
				);
			}
			sleep(poll);
		}
	}

	method _metadata_files () {
		return _metadata_files_in_dir( self.config(){meta_dir} );
	}

	method list_installed ( options? ) {
		let codec := new JSON();
		let installed := [];
		for ( let file in self._metadata_files() ) {
			let data := codec.load(file);
			let valid := _validate_installed_metadata(
				data,
				file.to_String(),
			);
			installed.push(valid);
		}

		return installed.sort( function ( a, b ) {
			let name_cmp := a{name} cmp b{name};
			return name_cmp if name_cmp != 0;
			return compare_versions( b{version}, a{version} );
		} );
	}

	method find_installed_module ( module_name, options? ) {
		let wanted := _module_key(module_name);
		for ( let dist in self.list_installed(options) ) {
			for ( let module in dist{installed}{modules} ) {
				let installed_as := _module_key(
					module{install_as},
				);
				if ( installed_as eq wanted ) {
					return dist;
				}
			}
		}
		return null;
	}

	method find_installed_distribution ( distribution_name, options? ) {
		let wanted := "" _ distribution_name;
		for ( let dist in self.list_installed(options) ) {
			return dist if dist{name} eq wanted;
		}
		return null;
	}

	method query ( module_name, options? ) {
		return self.find_installed_module( module_name, options );
	}

	method query_distribution ( distribution_name, options? ) {
		return self.find_installed_distribution(
			distribution_name,
			options,
		);
	}

	method is_installed ( module_name, min_version? ) {
		let found := self.find_installed_module(module_name);
		return false if found instanceof Null;
		if ( not( min_version instanceof Null ) ) {
			let comparison := compare_versions(
				found{version},
				min_version,
			);
			return comparison >= 0;
		}
		return true;
	}

	method installed_version ( module_name ) {
		let found := self.find_installed_module(module_name);
		return found instanceof Null ? null : found{version};
	}

	method pretty_json ( value ) {
		let codec := new JSON( pretty: true, canonical: true );
		return codec.encode(value);
	}

	method format_json ( value, options? ) {
		return self.pretty_json(value);
	}

	method _verify_module_file ( result, dist, entry ) {
		let path := _path_child(
			dist{installed}{lib_dir},
			entry{install_as},
		);
		let file := {
			kind: "module",
			distribution: dist{name},
			version: dist{version},
			install_as: entry{install_as},
			path: path.to_String(),
			exists: path.exists(),
			expected_sha256: entry{sha256},
			expected_size: entry.get( "size", null ),
			actual_sha256: null,
			actual_size: null,
			hash_ok: false,
			size_ok: null,
		};
		if ( path.exists() ) {
			let bytes := path.slurp();
			file{actual_sha256} := sha256_hex(bytes);
			file{actual_size} := path.size();
			file{hash_ok} := file{actual_sha256} eq entry{sha256};
			if ( file{expected_size} != null ) {
				file{size_ok} := file{actual_size} == file{expected_size};
				if ( not file{size_ok} ) {
					result{size_mismatches}.push(file);
				}
			}
			if ( not file{hash_ok} ) {
				result{hash_mismatches}.push(file);
			}
		}
		else {
			result{missing_files}.push(file);
		}
		result{files}.push(file);
		result{checked_files}.push(file);
		return file;
	}

	method _verify_script_file ( result, dist, entry ) {
		let path := _path_child(
			dist{installed}{bin_dir},
			entry{install_as},
		);
		let file := {
			kind: "script",
			distribution: dist{name},
			version: dist{version},
			install_as: entry{install_as},
			path: path.to_String(),
			exists: path.exists(),
			expected_sha256: entry{sha256},
			expected_size: entry.get( "size", null ),
			actual_sha256: null,
			actual_size: null,
			hash_ok: false,
			size_ok: null,
		};
		if ( path.exists() ) {
			let bytes := path.slurp();
			file{actual_sha256} := sha256_hex(bytes);
			file{actual_size} := path.size();
			file{hash_ok} := file{actual_sha256} eq entry{sha256};
			if ( file{expected_size} != null ) {
				file{size_ok} := file{actual_size} == file{expected_size};
				if ( not file{size_ok} ) {
					result{size_mismatches}.push(file);
				}
			}
			if ( not file{hash_ok} ) {
				result{hash_mismatches}.push(file);
			}
		}
		else {
			result{missing_files}.push(file);
		}
		result{files}.push(file);
		result{checked_files}.push(file);

		for ( let wrapper in entry.get( "wrappers", [] ) ) {
			let wrapper_path := _path_child(
				dist{installed}{bin_dir},
				wrapper,
			);
			let wrapper_file := {
				kind: "wrapper",
				distribution: dist{name},
				version: dist{version},
				install_as: wrapper,
				path: wrapper_path.to_String(),
				exists: wrapper_path.exists(),
				expected_sha256: null,
				actual_sha256: null,
				hash_ok: null,
			};
			if ( not wrapper_file{exists} ) {
				result{missing_files}.push(wrapper_file);
			}
			result{files}.push(wrapper_file);
			result{wrapper_files}.push(wrapper_file);
		}
		return file;
	}

	method verify ( targets, options? ) {
		let roots := self.dependency_roots(options);
		let target_root := roots[0];
		let installed := _list_installed_in_root(target_root);
		let result := {
			ok: true,
			target_root: target_root,
			targets: [],
			distributions: [],
			files: [],
			checked_files: [],
			wrapper_files: [],
			missing_files: [],
			hash_mismatches: [],
			size_mismatches: [],
			skipped_duplicates: [],
			errors: [],
		};

		let seen := {};
		for ( let target in _target_list(targets) ) {
			let record := _remove_target_record( target, options );
			if ( record{type} eq "dist" ) {
				record{type} := "distribution";
			}
			record{matches} := [];
			result{targets}.push(record);

			if (
				(
					record{type} ne "module" and
					record{type} ne "distribution"
				) or
				record{value} eq ""
			) {
				_add_verify_target_error(
					result,
					"invalid-target",
					record,
					"verify target must be a module or distribution",
				);
				next;
			}

			let matches := record{type} eq "module"
				? self._module_remove_matches(
					installed,
					record{value},
				)
				: self._distribution_remove_matches(
					installed,
					record{value},
				);
			for ( let match in matches ) {
				record{matches}.push(
					{
						name: match{name},
						version: match{version},
						metadata_file: match{metadata_file},
					},
				);
			}

			if ( matches.length() == 0 ) {
				_add_verify_target_error(
					result,
					"missing-target",
					record,
					"verify target is not installed",
				);
				next;
			}
			if ( record{type} eq "module" and matches.length() > 1 ) {
				_add_verify_target_error(
					result,
					"ambiguous-target",
					record,
					"module target has multiple owners",
				);
				result{errors}[ result{errors}.length() - 1 ]{matches} :=
					record{matches};
				next;
			}

			for ( let dist in matches ) {
				let key := dist{metadata_file};
				if ( seen.exists(key) ) {
					result{skipped_duplicates}.push(
						{
							target: record,
							name: dist{name},
							version: dist{version},
							metadata_file: dist{metadata_file},
						},
					);
					next;
				}
				seen.add( key, true );
				result{distributions}.push(
					{
						name: dist{name},
						version: dist{version},
						metadata_file: dist{metadata_file},
					},
				);
				for ( let module in dist{installed}{modules} ) {
					self._verify_module_file(
						result,
						dist,
						module,
					);
				}
				for ( let script in dist{installed}{scripts} ) {
					self._verify_script_file(
						result,
						dist,
						script,
					);
				}
			}
		}

		result{ok} := (
			result{errors}.length() == 0 and
			result{missing_files}.length() == 0 and
			result{hash_mismatches}.length() == 0 and
			result{size_mismatches}.length() == 0
		);
		return result;
	}

	method latest ( module_name, options? ) {
		let module := _latest_module_name(module_name);
		let base := _trim_right_slash(
			"" _ _opt( options, "base_url", base_url ),
		);
		let url := base _ "/module/" _ module _ ".json";
		let ua := _opt( options, "user_agent", self._user_agent() );
		let req := ua.build_request( "GET", url );
		_progress( options, "downloading latest metadata " _ url );
		let response := ua.send(req);
		response.expect_success();
		let remote_url := url;
		if ( response can "url" ) {
			remote_url := response.url();
		}
		_progress( options, "received latest metadata " _ remote_url );

		let metadata := _validate_source_metadata(
			response.json(),
			remote_url,
		);
		if ( not metadata.exists("status") ) {
			metadata{status} := "stable";
		}
		if (
			metadata.exists("status") and
			metadata{status} eq "trial"
		) {
			die(
				"Trial distributions are not available through " _
				"module-name latest endpoints"
			);
		}

		let installed := self.find_installed_module( module, options );
			let installed_version := installed instanceof Null
			? null
			: installed{version};
		let status := _latest_status(
			installed_version,
			metadata{version},
		);

		return {
			ok: true,
			module_name: module,
			status: status,
			can_upgrade: status eq "outdated",
			installed_version: installed_version,
			remote_version: metadata{version},
			remote_distribution: metadata{name},
			remote_status: metadata{status},
			url: url,
			remote_url: remote_url,
			installed: installed,
			remote_metadata: metadata,
		};
	}

	method can_upgrade ( module_name, options? ) {
		return self.latest( module_name, options );
	}

	method fetch_source ( target, options? ) {
		let value := "" _ target;
		let file := new Path(value);
		if ( file.exists() and file.is_file() ) {
			_progress( options, "using local archive " _ file.to_String() );
			let source := {
				type: "file",
				value: value,
				path: file.to_String(),
				path_obj: file,
			};
			_check_expected_source_sha(
				source,
				_opt( options, "expected_sha256", null ),
			);
			return source;
		}

		let source_type := _is_url(value) ? "url" : "module";
		let url := value;
		if ( source_type eq "module" ) {
			let base := _trim_right_slash(
				"" _ _opt( options, "base_url", base_url ),
			);
			url := base _ "/module/" _ value;
		}

		let cache_dir := _opt( options, "cache_dir", null );
		if ( not( cache_dir instanceof Null ) ) {
			let paths := _cache_paths( cache_dir, url );
			let cached := _validate_cache_entry(paths);
			if ( cached ) {
				_progress(
					options,
					"using cached archive for " _ value _ " from " _
						paths{archive}.to_String(),
				);
				let source := {
					type: source_type,
					value: value,
					url: url,
					resolved_url: cached{resolved_url},
					path: paths{archive}.to_String(),
					path_obj: paths{archive},
					cache_hit: true,
					cache_sidecar: paths{sidecar}.to_String(),
				};
				_check_expected_source_sha(
					source,
					_opt( options, "expected_sha256", null ),
				);
				return source;
			}
			_delete_cache_entry(paths);
		}

		let temp_dir := _new_temp_dir( options, "zuzuzoo-source-" );
		let temp_path := temp_dir.child("source.archive");
		let ua := _opt( options, "user_agent", self._user_agent() );
		let req := ua.build_request( "GET", url )
			.download_to( temp_path.to_String() );
		_progress( options, "downloading " _ value _ " from " _ url );
		let response := ua.send(req);

		let download_url := url;
		if ( not _response_success(response) and source_type eq "module" ) {
			_progress(
				options,
				"module archive download failed from " _ url _ " (" _
					_response_status_text(response) _
					"); checking latest metadata",
			);
			_cleanup_path(temp_path);

			let latest_info := self.latest( value, options );
			let archive_url := _archive_url_from_latest(latest_info);
			req := ua.build_request( "GET", archive_url )
				.download_to( temp_path.to_String() );
			_progress(
				options,
				"downloading " _ value _ " from " _ archive_url,
			);
			response := ua.send(req);
			download_url := archive_url;
		}

		if ( not _response_success(response) ) {
			_cleanup_path(temp_dir);
			die(
				"Source download failed (target=" _ value _
				", source_type=" _ source_type _
				", url=" _ download_url _
				"): HTTP request failed (" _
					_response_status_text(response) _ ")"
			);
		}

		let resolved_url := download_url;
		if ( response can "url" ) {
			resolved_url := response.url();
		}
		_progress(
			options,
			"downloaded " _ value _ " from " _ resolved_url _ " to " _
				temp_path.to_String(),
		);

		let source := {
			type: source_type,
			value: value,
			url: url,
			resolved_url: resolved_url,
			path: temp_path.to_String(),
			path_obj: temp_path,
			temp_dir: temp_dir,
		};
		_check_expected_source_sha(
			source,
			_opt( options, "expected_sha256", null ),
		);

		if ( not( cache_dir instanceof Null ) ) {
			let paths := _cache_paths( cache_dir, url );
			try {
				let sidecar := _write_cache_entry(
					paths,
					source,
					temp_path,
				);
				source{path} := paths{archive}.to_String();
				source{path_obj} := paths{archive};
				source{cache_hit} := false;
				source{cache_sidecar} := paths{sidecar}.to_String();
				source{cache_metadata} := sidecar;
				_progress(
					options,
					"cached " _ value _ " at " _
						paths{archive}.to_String(),
				);
				_cleanup_path(temp_dir);
			}
			catch ( Exception e ) {
				_cleanup_path(temp_dir);
				die _corrupt_archive_error( source, e );
			}
		}

		return source;
	}

	method load_distribution ( target, options? ) {
		let source := null;
		let work_dir := null;
		try {
			source := self.fetch_source( target, options );
			let source_path := new Path( source{path} );
			let archive := null;
			try {
				archive := Archive.decode( source_path.slurp() );
			}
			catch ( Exception e ) {
				die _corrupt_archive_error( source, e );
			}
			let root_name := _safe_archive_root( archive, source{path} );
			work_dir := _new_temp_dir( options, "zuzuzoo-work-" );
			let root_dir := work_dir.child(root_name);
			root_dir.mkdir();
			_progress(
				options,
				"extracting " _ source{path} _ " to " _
					work_dir.to_String(),
			);
			_extract_archive( archive, root_name, root_dir );

			let metadata_file := root_dir.child("zuzu-distribution.json");
			if ( not metadata_file.exists() ) {
				die(
					`Invalid archive ${source{path}}: ` _
					"missing zuzu-distribution.json"
				);
			}
			let codec := new JSON( pairlists: true );
			let metadata := _validate_source_metadata(
				codec.load(metadata_file),
				metadata_file.to_String(),
			);

			let expected_root := metadata{name} _ "-" _ metadata{version};
			if ( root_name ne expected_root ) {
				die(
					`Invalid archive ${source{path}}: root ${root_name} ` _
					`does not match ${expected_root}`
				);
			}

			let build_result := null;
			let build_file := root_dir.child("Build.zzs");
			if ( build_file.exists() and build_file.is_file() ) {
				_progress(
					options,
					"building " _ metadata{name} _ " " _
						metadata{version} _ " with Build.zzs",
				);
				build_result := Proc.run(
					_opt( options, "zuzu_command", zuzu_command ),
					[ "Build.zzs" ],
					{
						cwd: root_dir.to_String(),
						capture_stdout: true,
						capture_stderr: true,
					},
				);
				if ( not Proc.is_success(build_result) ) {
					die(
						"Build.zzs failed: " _
						Proc.status_text(build_result)
					);
				}
				metadata := _validate_source_metadata(
					codec.load(metadata_file),
					metadata_file.to_String(),
				);
				let post_build_root := metadata{name} _ "-" _
					metadata{version};
				if ( root_name ne post_build_root ) {
					die(
						`Invalid archive ${source{path}}: root ${root_name} ` _
						`does not match ${post_build_root}`
					);
				}
			}

			let module_sources := _discover_files(
				root_dir,
				"modules",
				".zzm",
			);
			let script_sources := _discover_scripts(root_dir);
			let tests := _discover_files( root_dir, "tests", ".zzs" );
			let modules := module_sources.map( function ( source ) {
				return {
					source: source,
					install_as: _module_install_name(source),
				};
			} );
			let scripts := script_sources.map( function ( source ) {
				return {
					source: source,
					install_as: _script_install_name(source),
				};
			} );

			let loaded := {
				source: source,
				archive: archive,
				work_dir: work_dir.to_String(),
				root: root_dir.to_String(),
				work_dir_obj: work_dir,
				root_obj: root_dir,
				root_name: root_name,
				metadata: metadata,
				modules: modules,
				scripts: scripts,
				tests: tests,
				build: build_result,
			};
			if ( not _opt( options, "keep_work_dirs", false ) ) {
				_cleanup_source( source, options );
				_cleanup_path(work_dir);
			}
			return loaded;
		}
		catch ( Exception e ) {
			_cleanup_source( source, options );
			_cleanup_path(work_dir)
				if not _opt( options, "keep_work_dirs", false );
			throw e;
		}
	}

	method dependency_roots ( options? ) {
		let cfg := self.config();
		let target := _root_from_config( "target", "target", cfg );
		let roots := [];
		let seen := {};
		_add_root( roots, seen, target );

		if ( not cfg{global} and not cfg{windows} ) {
			let global_override := _opt(
				options,
				"global_root",
				global_root,
			);
			let global_dependency_root := null;
			if ( not( global_override instanceof Null ) ) {
				global_dependency_root := _root_override(
					global_override,
					"global",
					"global",
					"global",
				);
			}
			else {
				let global_cfg := new Zuzuzoo(
					global: true,
					windows: false,
					home: home,
					userprofile: userprofile,
				).config();
				global_dependency_root := _root_from_config(
					"global",
					"global",
					global_cfg,
				);
			}
			_add_root(
				roots,
				seen,
				global_dependency_root,
			);
		}

		let user_cfg := new Zuzuzoo(
			global: false,
			windows: cfg{windows},
			home: home,
			userprofile: userprofile,
		).config();
		_add_root(
			roots,
			seen,
			_root_from_config( "user", "user", user_cfg ),
		);

		let ctor_roots := dependency_roots instanceof Null
			? []
			: dependency_roots;
		for ( let root in ctor_roots ) {
			let where := (
				root instanceof Dict and root.exists("name")
			)
				? root{name}
				: "constructor";
			_add_root(
				roots,
				seen,
				_custom_root( root, where ),
			);
		}

		let option_roots := _opt( options, "dependency_roots", [] );
		for ( let root in option_roots ) {
			let where := (
				root instanceof Dict and root.exists("name")
			)
				? root{name}
				: "options";
			_add_root(
				roots,
				seen,
				_custom_root( root, where ),
			);
		}

		return roots;
	}

	method find_dependency ( module_name, min_version?, options? ) {
		let planned := _opt( options, "planned_installs", [] );
		return _find_dependency_for_plan(
			planned,
			self.dependency_roots(options),
			module_name,
			min_version instanceof Null ? "0" : min_version,
		);
	}

	method _add_removal ( removals, removal_seen, dist, root, reason ) {
		let key := dist{metadata_file};
		if ( removal_seen.exists(key) ) {
			removal_seen.get(key){reasons}.push(reason);
			return false;
		}
		let removal := {
			name: dist{name},
			version: dist{version},
			metadata_file: dist{metadata_file},
			root: root,
			reasons: [ reason ],
			distribution: dist,
		};
		removal_seen.add( key, removal );
		removals.push(removal);
		return true;
	}

	method _plan_target_root_removals (
		plan,
		target_installed,
		target_root
	) {
		let removal_seen := {};
			for ( let install_action in plan{installs} ) {
				for ( let dist in target_installed ) {
					if ( dist{name} eq install_action{metadata}{name} ) {
						let reason := dist{version} eq install_action{metadata}{version}
							? "reinstall"
							: "prior-version";
					self._add_removal(
						plan{removals},
						removal_seen,
						dist,
						target_root,
						reason,
					);
				}
			}
		}

			for ( let install_action in plan{installs} ) {
				for ( let module in install_action{modules} ) {
					let destination := _path_join(
						target_root{lib_dir},
					module{install_as},
					target_root{windows},
				);
				for ( let dist in target_installed ) {
					for ( let owned in dist{installed}{modules} ) {
						if ( owned{install_as} eq module{install_as} ) {
							plan{ownership_conflicts}.push(
									{
										kind: "module",
										install_as: module{install_as},
										destination: destination,
										planned_distribution: install_action{metadata}{name},
										planned_version: install_action{metadata}{version},
										owner_distribution: dist{name},
									owner_version: dist{version},
									owner_metadata_file: dist{metadata_file},
									root: target_root,
								},
							);
							self._add_removal(
								plan{removals},
								removal_seen,
								dist,
								target_root,
								"owner-conflict",
							);
						}
					}
				}
			}

				for ( let script in install_action{scripts} ) {
					let destination := _path_join(
					target_root{bin_dir},
					script{install_as},
					target_root{windows},
				);
				for ( let dist in target_installed ) {
					for ( let owned in dist{installed}{scripts} ) {
						if ( owned{install_as} eq script{install_as} ) {
							plan{ownership_conflicts}.push(
									{
										kind: "script",
										install_as: script{install_as},
										destination: destination,
										planned_distribution: install_action{metadata}{name},
										planned_version: install_action{metadata}{version},
										owner_distribution: dist{name},
									owner_version: dist{version},
									owner_metadata_file: dist{metadata_file},
									root: target_root,
								},
							);
							self._add_removal(
								plan{removals},
								removal_seen,
								dist,
								target_root,
								"owner-conflict",
							);
						}
					}
				}
			}
		}
		return plan;
	}

	method _module_remove_matches ( installed, module_name ) {
		let wanted := _module_key(module_name);
		let matches := [];
		for ( let dist in installed ) {
			for ( let module in dist{installed}{modules} ) {
				if ( _module_key( module{install_as} ) eq wanted ) {
					matches.push(dist);
					last;
				}
			}
		}
		return matches;
	}

	method _distribution_remove_matches ( installed, distribution_name ) {
		let wanted := "" _ distribution_name;
		let matches := [];
		for ( let dist in installed ) {
			matches.push(dist) if dist{name} eq wanted;
		}
		return matches;
	}

	method _remove_owner_map ( installed, target_root ) {
		let by_path := {};
		for ( let dist in installed ) {
			let owner := _owner_record(dist);
			for ( let module in dist{installed}{modules} ) {
				_add_owner_for_path(
					by_path,
					_path_join(
						target_root{lib_dir},
						module{install_as},
						target_root{windows},
					),
					owner,
				);
			}
			for ( let script in dist{installed}{scripts} ) {
				_add_owner_for_path(
					by_path,
					_path_join(
						target_root{bin_dir},
						script{install_as},
						target_root{windows},
					),
					owner,
				);
				for ( let wrapper in script.get( "wrappers", [] ) ) {
					_add_owner_for_path(
						by_path,
						_path_join(
							target_root{bin_dir},
							wrapper,
							target_root{windows},
						),
						owner,
					);
				}
			}
		}
		return by_path;
	}

	method _add_remove_file (
		plan,
		file_seen,
		owner_map,
		planned_metadata,
		kind,
		path,
		dist,
		install_as
	) {
		return false if file_seen.exists(path);
		file_seen.add( path, true );

		let owners := kind eq "metadata"
			? [ _owner_record(dist) ]
			: owner_map.get( path, [ _owner_record(dist) ] );
		let planned_owners := [];
		let kept_owners := [];
		for ( let owner in owners ) {
			if ( planned_metadata.exists(owner{metadata_file}) ) {
				planned_owners.push(owner);
			}
			else {
				kept_owners.push(owner);
			}
		}

		let blocked := kind ne "metadata" and kept_owners.length() > 0;
		let file := {
			kind: kind,
			path: path,
			exists: ( new Path(path) ).exists(),
			owners: owners,
			planned_owners: planned_owners,
			kept_owners: kept_owners,
			blocked: blocked,
		};
		file{install_as} := install_as if not( install_as instanceof Null );
		plan{files}.push(file);

		if ( blocked ) {
			let conflict := {
				kind: kind,
				path: path,
				install_as: install_as,
				owners: owners,
				planned_owners: planned_owners,
				kept_owners: kept_owners,
			};
			plan{shared_file_conflicts}.push(conflict);
			plan{errors}.push(
				{
					code: "shared-file-conflict",
					message: "file is also owned by a kept distribution",
					path: path,
					owners: owners,
					kept_owners: kept_owners,
				},
			);
		}
		return true;
	}

	method _build_remove_files ( plan, installed, target_root ) {
		let owner_map := self._remove_owner_map( installed, target_root );
		let planned_metadata := {};
		for ( let removal in plan{removals} ) {
			planned_metadata.add( removal{metadata_file}, true );
		}

		let file_seen := {};
		for ( let removal in plan{removals} ) {
			let dist := removal{distribution};
			for ( let module in dist{installed}{modules} ) {
				self._add_remove_file(
					plan,
					file_seen,
					owner_map,
					planned_metadata,
					"module",
					_path_join(
						target_root{lib_dir},
						module{install_as},
						target_root{windows},
					),
					dist,
					module{install_as},
				);
			}
			for ( let script in dist{installed}{scripts} ) {
				self._add_remove_file(
					plan,
					file_seen,
					owner_map,
					planned_metadata,
					"script",
					_path_join(
						target_root{bin_dir},
						script{install_as},
						target_root{windows},
					),
					dist,
					script{install_as},
				);
				for ( let wrapper in script.get( "wrappers", [] ) ) {
					self._add_remove_file(
						plan,
						file_seen,
						owner_map,
						planned_metadata,
						"wrapper",
						_path_join(
							target_root{bin_dir},
							wrapper,
							target_root{windows},
						),
						dist,
						wrapper,
					);
				}
			}
		}

		for ( let removal in plan{removals} ) {
			self._add_remove_file(
				plan,
				file_seen,
				owner_map,
				planned_metadata,
				"metadata",
				removal{metadata_file},
				removal{distribution},
				null,
			);
		}

		plan{files} := plan{files}.sort(_remove_file_cmp);
		plan{shared_file_conflicts} :=
			plan{shared_file_conflicts}.sort(
				fn ( a, b ) -> a{path} cmp b{path},
			);
		return plan;
	}

	method plan_remove ( targets, options? ) {
		let roots := self.dependency_roots(options);
		let target_root := roots[0];
		let installed := _list_installed_in_root(target_root);
		let plan := {
			ok: true,
			target_root: target_root,
			targets: [],
			removals: [],
			files: [],
			shared_file_conflicts: [],
			skipped_duplicates: [],
			errors: [],
		};

		let removal_seen := {};
		for ( let target in _target_list(targets) ) {
			let record := _remove_target_record( target, options );
			if ( record{type} eq "dist" ) {
				record{type} := "distribution";
			}
			record{matches} := [];
			plan{targets}.push(record);

			if (
				(
					record{type} ne "module" and
					record{type} ne "distribution"
				) or
				record{value} eq ""
			) {
				plan{errors}.push(
					{
						code: "invalid-target",
						target: record,
						message: "remove target must be a module or distribution",
					},
				);
				next;
			}

			let matches := record{type} eq "module"
				? self._module_remove_matches(
					installed,
					record{value},
				)
				: self._distribution_remove_matches(
					installed,
					record{value},
				);
			for ( let match in matches ) {
				record{matches}.push(
					{
						name: match{name},
						version: match{version},
						metadata_file: match{metadata_file},
					},
				);
			}

			if ( matches.length() == 0 ) {
				plan{errors}.push(
					{
						code: "missing-target",
						target: record,
						message: "remove target is not installed",
					},
				);
				next;
			}
			if ( record{type} eq "module" and matches.length() > 1 ) {
				plan{errors}.push(
					{
						code: "ambiguous-target",
						target: record,
						message: "module target has multiple owners",
						matches: record{matches},
					},
				);
				next;
			}

			for ( let dist in matches ) {
				let added := self._add_removal(
					plan{removals},
					removal_seen,
					dist,
					target_root,
					record{type} eq "module"
						? "requested-module"
						: "requested-distribution",
				);
				if ( not added ) {
					plan{skipped_duplicates}.push(
						{
							target: record,
							name: dist{name},
							version: dist{version},
							metadata_file: dist{metadata_file},
						},
					);
				}
			}
		}

		plan{removals} := plan{removals}.sort(_removal_cmp);
		self._build_remove_files( plan, installed, target_root );
		plan{ok} := (
			plan{errors}.length() == 0 and
			plan{shared_file_conflicts}.length() == 0
		);
		return plan;
	}

	method plan_install ( targets, options? ) {
		let roots := self.dependency_roots(options);
		let target_root := roots[0];
		let plan := {
			target_root: target_root,
			dependency_roots: roots,
			installs: [],
			removals: [],
			satisfied_dependencies: [],
			dependency_graph: {
				nodes: [],
				edges: [],
			},
			ownership_conflicts: [],
		};

		let planned := [];
		let planned_by_name := {};
		let status_by_name := {};
		let seen_targets := {};
		let loaded_work := [];

		function resolve ( target, requested, dependency_of, min_version, stack ) {
			let target_text := "" _ target;
			if ( requested ) {
				return null if seen_targets.exists(target_text);
				seen_targets.add( target_text, true );
			}
			else {
				let found := _find_dependency_for_plan(
					planned,
					roots,
					target_text,
					min_version,
				);
				if ( not( found instanceof Null ) ) {
					if (
						found{source} eq "planned" and
						status_by_name.get(found{distribution}, "") eq "visiting"
					) {
						die "Dependency cycle detected: " _
							_cycle_path( stack, target_text );
					}
					let satisfied := found;
					satisfied{requested_by} := dependency_of instanceof Null
						? null
						: dependency_of{metadata}{name};
					plan{satisfied_dependencies}.push(satisfied);
					return null;
				}
				if ( _stack_contains( stack, target_text ) ) {
					die "Dependency cycle detected: " _
						_cycle_path( stack, target_text );
				}
			}

			let dist := self.load_distribution(
				target_text,
				_copy_options_with( options, "keep_work_dirs", true ),
			);
			loaded_work.push(dist);
			if (
				dist{source}{type} eq "module" and
				dist{metadata}.exists("status") and
				dist{metadata}{status} eq "trial"
			) {
				die(
					"Trial distributions are not available through " _
					"module-name endpoints"
				);
			}
			if (
				not requested and
				not _loaded_distribution_provides(
					dist,
					target_text,
					min_version,
				)
			) {
				let dep := {
					module_name: target_text,
					min_version: min_version,
				};
				_cleanup_source( dist{source}, options );
				_cleanup_path(dist{work_dir_obj})
					if not _opt( options, "keep_work_dirs", false );
				die _dependency_conflict_message(
					dep,
					dependency_of,
					stack,
					null,
					dist,
				);
			}

			let dist_name := dist{metadata}{name};
			if ( planned_by_name.exists(dist_name) ) {
				let existing := planned_by_name.get(dist_name);
				if (
					existing{metadata}{version} ne
					dist{metadata}{version}
				) {
					_cleanup_source( dist{source}, options );
					_cleanup_path(dist{work_dir_obj})
						if not _opt( options, "keep_work_dirs", false );
					if ( requested ) {
						die _planned_version_conflict_message(
							dist_name,
							existing,
							dist,
							target_text,
						);
					}
					let dep := {
						module_name: target_text,
						min_version: min_version,
					};
					die _dependency_conflict_message(
						dep,
						dependency_of,
						stack,
						existing,
						dist,
					);
				}
				_cleanup_source( dist{source}, options );
				_cleanup_path(dist{work_dir_obj})
					if not _opt( options, "keep_work_dirs", false );
				return existing;
			}

			let dependencies := _dependency_entries(dist{metadata});
			let install_action := {
				action: "install",
				target: target_text,
				requested: requested ? true : false,
				dependency_of: dependency_of instanceof Null
					? null
					: dependency_of{metadata}{name},
				source: dist{source},
				metadata: dist{metadata},
				modules: dist{modules},
				scripts: dist{scripts},
				tests: dist{tests},
				dependencies: dependencies,
				target_root: target_root,
				root: dist{root},
				work_dir: dist{work_dir},
				root_obj: dist{root_obj},
				work_dir_obj: dist{work_dir_obj},
			};
			planned.push(install_action);
			planned_by_name.add( dist_name, install_action );
			status_by_name.add( dist_name, "visiting" );
			plan{dependency_graph}{nodes}.push(
				{
					name: dist_name,
					version: dist{metadata}{version},
					target: target_text,
					requested: requested ? true : false,
				},
			);

			let next_stack := stack;
			if ( dist{source}{type} eq "module" ) {
				next_stack := [];
				for ( let item in stack ) {
					next_stack.push(item);
				}
				next_stack.push(target_text);
			}

			for ( let dep in dependencies ) {
				let found := _find_dependency_for_plan(
					planned,
					roots,
					dep{module_name},
					dep{min_version},
				);
				if ( not( found instanceof Null ) ) {
					if (
						found{source} eq "planned" and
						status_by_name.get(found{distribution}, "") eq "visiting"
					) {
						die "Dependency cycle detected: " _
							_cycle_path( next_stack, dep{module_name} );
					}
					let satisfied := found;
					satisfied{requested_by} := dist_name;
					plan{satisfied_dependencies}.push(satisfied);
					plan{dependency_graph}{edges}.push(
						{
							from: dist_name,
							module_name: dep{module_name},
							min_version: dep{min_version},
							status: found{source} eq "planned"
								? "planned"
								: "satisfied",
							to: found{distribution},
							root: found.exists("root") ? found{root} : null,
						},
					);
				}
				else {
					let dep_install := resolve(
						dep{module_name},
						false,
						install_action,
						dep{min_version},
						next_stack,
					);
					plan{dependency_graph}{edges}.push(
						{
							from: dist_name,
							module_name: dep{module_name},
							min_version: dep{min_version},
							status: "planned",
							to: dep_install instanceof Null
								? null
								: dep_install{metadata}{name},
							root: target_root,
						},
					);
				}
			}

			status_by_name.set( dist_name, "done" );
			plan{installs}.push(install_action);
			return install_action;
		}

		try {
			for ( let target in _target_list(targets) ) {
				resolve( target, true, null, "0", [] );
			}
		}
		catch ( Exception e ) {
			_cleanup_loaded_work_dirs(loaded_work, options);
			throw e;
		}

		self._plan_target_root_removals(
			plan,
			_list_installed_in_root(target_root),
			target_root,
		);

		return plan;
	}

	method run_distribution_tests ( install_action, options? ) {
		let results := [];
		let include_dirs := [];
		let seen_include_dirs := {};
		let own_modules := _path_child( install_action{root}, "modules" );
		_push_unique_string(
			include_dirs,
			seen_include_dirs,
			own_modules.to_String(),
		) if own_modules.exists() and own_modules.is_dir();
		for ( let include_dir in _opt( options, "test_include_dirs", [] ) ) {
			_push_unique_string(
				include_dirs,
				seen_include_dirs,
				include_dir,
			);
		}

		for ( let test in install_action{tests} ) {
			_progress(
				options,
				"testing " _ install_action{metadata}{name} _ " " _
					install_action{metadata}{version} _ ": " _ test,
			);
			let argv := include_dirs.map( fn d -> "-I" _ d );
			argv.push(test);
			let run_result := Proc.run(
				_opt( options, "zuzu_command", zuzu_command ),
				argv,
				{
					cwd: install_action{root},
					capture_stdout: true,
					capture_stderr: true,
				},
			);
			let parsed := parse_tap(run_result{stdout});
			results.push(
				{
					test: test,
					ok: _test_ok( parsed, run_result ),
					status: Proc.status_text(run_result),
					result: run_result,
					tap: parsed,
					stdout: run_result{stdout},
					stderr: run_result{stderr},
				},
			);
		}
		let ok := true;
		for ( let result in results ) {
			ok := false if not result{ok};
		}
		return {
			ok: ok,
			tests: results,
			distribution: install_action{metadata}{name},
			version: install_action{metadata}{version},
		};
	}

	method execute_removal ( removal_action, options? ) {
		_progress(
			options,
			"removing " _ removal_action{name} _ " " _
				removal_action{version},
		);
		let warnings := [];
		let removed := [];
		let dist := removal_action{distribution};
		let root := removal_action{root};

		function remove_file ( path, kind ) {
			if ( path.exists() ) {
				path.remove();
				removed.push(
					{
						kind: kind,
						path: path.to_String(),
					},
				);
			}
			else {
				warnings.push(
					{
						kind: kind,
						path: path.to_String(),
						message: "missing file",
					},
				);
			}
		}

		for ( let module in dist{installed}{modules} ) {
			remove_file(
				_path_child( root{lib_dir}, module{install_as} ),
				"module",
			);
		}
		for ( let script in dist{installed}{scripts} ) {
			remove_file(
				_path_child( root{bin_dir}, script{install_as} ),
				"script",
			);
			for ( let wrapper in script.get( "wrappers", [] ) ) {
				remove_file(
					_path_child( root{bin_dir}, wrapper ),
					"wrapper",
				);
			}
		}
		remove_file( new Path( removal_action{metadata_file} ), "metadata" );

		return {
			ok: true,
			name: removal_action{name},
			version: removal_action{version},
			removed: removed,
			warnings: warnings,
		};
	}

	method _install_action ( install_action, installed_at, options? ) {
		let root := install_action{target_root};
		let module_records := [];
		let script_records := [];

		for ( let module in install_action{modules} ) {
			let source := _path_child(
				install_action{root},
				module{source},
			);
			let destination := _path_child(
				root{lib_dir},
				module{install_as},
			);
			_progress(
				options,
				"installing module " _ module{install_as} _ " to " _
					destination.to_String(),
			);
			let written := _copy_file_atomic(source, destination, null);
			module_records.push(
				{
					source: module{source},
					install_as: module{install_as},
					sha256: written{sha256},
					size: written{size},
				},
			);
		}

		for ( let script in install_action{scripts} ) {
			let source := _path_child(
				install_action{root},
				script{source},
			);
			let destination := _path_child(
				root{bin_dir},
				script{install_as},
			);
			_progress(
				options,
				"installing script " _ script{install_as} _ " to " _
					destination.to_String(),
			);
			let written := _copy_file_atomic(
				source,
				destination,
				root{windows} ? null : 493,
			);

			let wrappers := [];
			if ( root{windows} ) {
				let wrapper_name := _replace_script_suffix(
					script{install_as},
					".cmd",
				);
				let wrapper := _path_child( root{bin_dir}, wrapper_name );
				let script_base := _relative_basename(script{install_as});
				_progress(
					options,
					"installing command wrapper " _ wrapper_name _
						" to " _ wrapper.to_String(),
				);
				_spew_utf8_atomic(
					wrapper,
					"@echo off\r\n" _
					"zuzu \"%~dp0" _ script_base _ "\" %*\r\n",
				);
				wrappers.push(wrapper_name);
			}

			script_records.push(
				{
					source: script{source},
					install_as: script{install_as},
					sha256: written{sha256},
					size: written{size},
					wrappers: wrappers,
				},
			);
		}

		let metadata := _copy_source_metadata(install_action{metadata});
		metadata{installed} := {
			zdf: "ZDF-1",
			lib_dir: root{lib_dir},
			bin_dir: root{bin_dir},
			meta_dir: root{meta_dir},
			installed_at: installed_at,
			source: _copy_source_record(install_action{source}),
			modules: module_records,
			scripts: script_records,
		};

		let metadata_file := _path_child(
			root{meta_dir},
			metadata{name} _ "-" _ metadata{version} _ ".json",
		);
		_progress(
			options,
			"writing metadata for " _ metadata{name} _ " " _
				metadata{version} _ " to " _ metadata_file.to_String(),
		);
		_atomic_json_write( metadata_file, metadata );

		return {
			name: metadata{name},
			version: metadata{version},
			metadata_file: metadata_file.to_String(),
			modules: module_records,
			scripts: script_records,
			metadata: metadata,
		};
	}

	method format_install_plan ( plan ) {
		let lines := [
			"Install target:",
			"  lib: " _ plan{target_root}{lib_dir},
			"  bin: " _ plan{target_root}{bin_dir},
			"  meta: " _ plan{target_root}{meta_dir},
			"",
			"Removals:",
		];

		let removal_lines := [];
		for ( let removal in plan{removals} ) {
			removal_lines.push(
				"  - " _ removal{name} _ " " _ removal{version} _
				" [" _ join(
					", ",
					removal{reasons}.sort( fn ( a, b ) -> a cmp b ),
				) _ "]"
			);
		}
		removal_lines := removal_lines.sort( fn ( a, b ) -> a cmp b );
		if ( removal_lines.length() == 0 ) {
			lines.push("  none");
		}
		else {
			for ( let line in removal_lines ) {
				lines.push(line);
			}
		}

			lines.push("");
			lines.push("Installs:");
			let install_lines := [];
			for ( let install_action in plan{installs} ) {
				install_lines.push(
					"  - " _ install_action{metadata}{name} _ " " _
					install_action{metadata}{version}
				);
				for ( let module in install_action{modules} ) {
					install_lines.push(
						"    module " _ module{install_as} _ " -> " _
					_path_join(
						plan{target_root}{lib_dir},
						module{install_as},
						plan{target_root}{windows},
					)
					);
				}
				for ( let script in install_action{scripts} ) {
					install_lines.push(
						"    script " _ script{install_as} _ " -> " _
					_path_join(
						plan{target_root}{bin_dir},
						script{install_as},
						plan{target_root}{windows},
					)
				);
			}
		}
		if ( install_lines.length() == 0 ) {
			lines.push("  none");
		}
		else {
			for ( let line in install_lines ) {
				lines.push(line);
			}
		}

		lines.push("");
		lines.push("Conflicts:");
		let conflict_lines := [];
		for ( let conflict in plan{ownership_conflicts} ) {
			conflict_lines.push(
				"  - " _ conflict{kind} _ " " _
				conflict{install_as} _ " owned by " _
				conflict{owner_distribution} _ " " _
				conflict{owner_version} _ "; replacing with " _
				conflict{planned_distribution} _ " " _
				conflict{planned_version} _ "; destination " _
				conflict{destination} _ "; owner metadata " _
				conflict{owner_metadata_file}
			);
		}
		conflict_lines := conflict_lines.sort( fn ( a, b ) -> a cmp b );
		if ( conflict_lines.length() == 0 ) {
			lines.push("  none");
		}
		else {
			for ( let line in conflict_lines ) {
				lines.push(line);
			}
		}

		return join( "\n", lines ) _ "\n";
	}

	method format_remove_plan ( plan ) {
		let lines := [
			"Remove target:",
			"  lib: " _ plan{target_root}{lib_dir},
			"  bin: " _ plan{target_root}{bin_dir},
			"  meta: " _ plan{target_root}{meta_dir},
			"",
			"Removals:",
		];

		if ( plan{removals}.length() == 0 ) {
			lines.push("  none");
		}
		else {
			for ( let removal in plan{removals} ) {
				lines.push(
					"  - " _ removal{name} _ " " _
					removal{version}
				);
			}
		}

		lines.push("");
		lines.push("Files:");
		if ( plan{files}.length() == 0 ) {
			lines.push("  none");
		}
		else {
			for ( let file in plan{files} ) {
				let status := file{blocked}
					? "blocked"
					: ( file{exists} ? "exists" : "missing" );
				lines.push(
					"  - " _ file{kind} _ " " _
					file{path} _ " [" _ status _ "]"
				);
			}
		}

		lines.push("");
		lines.push("Shared file conflicts:");
		if ( plan{shared_file_conflicts}.length() == 0 ) {
			lines.push("  none");
		}
		else {
			for ( let conflict in plan{shared_file_conflicts} ) {
				let kept := conflict{kept_owners}.map(
					fn o -> o{name} _ " " _ o{version},
				).sort( fn ( a, b ) -> a cmp b );
				lines.push(
					"  - " _ conflict{path} _
					" kept by " _ join( ", ", kept )
				);
			}
		}

		lines.push("");
		lines.push("Errors:");
		if ( plan{errors}.length() == 0 ) {
			lines.push("  none");
		}
		else {
			for ( let error in plan{errors} ) {
				lines.push(
					"  - " _ error{code} _ ": " _
					error{message}
				);
			}
		}

		return join( "\n", lines ) _ "\n";
	}

	method _install_unlocked ( targets, options? ) {
		_progress(
			options,
			"planning install for " _ join( ", ", _target_list(targets) ),
		);
		let plan := self.plan_install( targets, options );
		let plan_text := self.format_install_plan(plan);
		STDOUT.print(plan_text) if _opt( options, "print_plan", false );

		if ( _opt( options, "dry_run", false ) ) {
			_cleanup_plan_work_dirs(plan, options);
			return {
				ok: true,
				dry_run: true,
				plan: plan,
				plan_text: plan_text,
				tests: [],
				removals: [],
				installs: [],
				warnings: [],
			};
		}

		let test_results := [];
		let tests_ok := true;
		if ( not _opt( options, "no_test", false ) ) {
			let test_include_dirs := [];
			let seen_test_include_dirs := {};
			for ( let install_action in plan{installs} ) {
				let modules_dir := _path_child(
					install_action{root},
					"modules",
				);
				_push_unique_string(
					test_include_dirs,
					seen_test_include_dirs,
					modules_dir.to_String(),
				) if modules_dir.exists() and modules_dir.is_dir();
			}
			for ( let include_dir in _runtime_include_dirs() ) {
				_push_unique_string(
					test_include_dirs,
					seen_test_include_dirs,
					include_dir,
				);
			}
			for ( let root in self.dependency_roots(options) ) {
				_push_unique_string(
					test_include_dirs,
					seen_test_include_dirs,
					root{lib_dir},
				) if root{lib_dir} ne "";
			}
			let test_options := _copy_options_with(
				options,
				"test_include_dirs",
				test_include_dirs,
			);
			for ( let install_action in plan{installs} ) {
				let test_result := self.run_distribution_tests(
					install_action,
					test_options,
				);
				test_results.push(test_result);
				tests_ok := false if not test_result{ok};
			}
		}

		if ( not tests_ok and not _opt( options, "force", false ) ) {
			_cleanup_plan_work_dirs(plan, options);
			return {
				ok: false,
				error: "distribution tests failed",
				dry_run: false,
				plan: plan,
				plan_text: plan_text,
				tests: test_results,
				removals: [],
				installs: [],
				warnings: [],
			};
		}

		try {
			let removals := [];
			let warnings := [];
			for ( let removal in plan{removals} ) {
				let result := self.execute_removal( removal, options );
				removals.push(result);
				for ( let warning in result{warnings} ) {
					warnings.push(warning);
				}
			}

			let installs := [];
			let installed_at := _installed_at();
			for ( let install_action in plan{installs} ) {
				installs.push( self._install_action(
					install_action,
					installed_at,
					options,
				) );
			}

			_cleanup_plan_work_dirs(plan, options);
			return {
				ok: true,
				dry_run: false,
				forced: not tests_ok and _opt( options, "force", false ),
				plan: plan,
				plan_text: plan_text,
				tests: test_results,
				removals: removals,
				installs: installs,
				warnings: warnings,
			};
		}
		catch ( Exception e ) {
			_cleanup_plan_work_dirs(plan, options);
			throw e;
		}
	}

	method install ( targets, options? ) {
		let lock := self.acquire_lock( "install", options );
		try {
			let result := self._install_unlocked( targets, options );
			lock.release();
			return result;
		}
		catch ( Exception e ) {
			lock.release();
			throw e;
		}
	}

	method _remove_unlocked ( targets, options? ) {
		let plan := self.plan_remove( targets, options );
		let plan_text := self.format_remove_plan(plan);
		STDOUT.print(plan_text) if _opt( options, "print_plan", false );

		if ( not plan{ok} ) {
			return {
				ok: false,
				dry_run: false,
				plan: plan,
				plan_text: plan_text,
				removed: [],
				warnings: [],
				errors: plan{errors},
			};
		}

		if ( _opt( options, "dry_run", false ) ) {
			return {
				ok: true,
				dry_run: true,
				plan: plan,
				plan_text: plan_text,
				removed: [],
				warnings: [],
				errors: [],
			};
		}

		let removed := [];
		let warnings := [];
		for ( let file in plan{files} ) {
			let path := new Path(file{path});
			if ( path.exists() ) {
				path.remove();
				removed.push(
					{
						kind: file{kind},
						path: file{path},
					},
				);
			}
			else {
				warnings.push(
					{
						kind: file{kind},
						path: file{path},
						message: "missing file",
					},
				);
			}
		}

		return {
			ok: true,
			dry_run: false,
			plan: plan,
			plan_text: plan_text,
			removed: removed,
			warnings: warnings,
			errors: [],
		};
	}

	method remove ( targets, options? ) {
		let lock := self.acquire_lock( "remove", options );
		try {
			let result := self._remove_unlocked( targets, options );
			lock.release();
			return result;
		}
		catch ( Exception e ) {
			lock.release();
			throw e;
		}
	}
}

function _zoo ( options? ) {
	return new Zuzuzoo(
		lib_dir: _opt( options, "lib_dir", null ),
		bin_dir: _opt( options, "bin_dir", null ),
		meta_dir: _opt( options, "meta_dir", null ),
		global: _opt( options, "global", false ),
		windows: _opt( options, "windows", null ),
		home: _opt( options, "home", null ),
		userprofile: _opt( options, "userprofile", null ),
		base_url: _opt( options, "base_url", "https://zuzulang.org" ),
		user_agent: _opt( options, "user_agent", null ),
		zuzu_command: _opt( options, "zuzu_command", "zuzu" ),
		dependency_roots: _opt( options, "dependency_roots", [] ),
		global_root: _opt( options, "global_root", null ),
	);
}

function list_installed ( options? ) {
	return _zoo(options).list_installed(options);
}

function query ( module_name, options? ) {
	return _zoo(options).query( module_name, options );
}

function query_distribution ( distribution_name, options? ) {
	return _zoo(options).query_distribution( distribution_name, options );
}

function is_installed ( module_name, min_version?, options? ) {
	return _zoo(options).is_installed( module_name, min_version );
}

function installed_version ( module_name, options? ) {
	return _zoo(options).installed_version(module_name);
}

function pretty_json ( value, options? ) {
	return _zoo(options).pretty_json(value);
}

function format_json ( value, options? ) {
	return _zoo(options).format_json( value, options );
}

function fetch_source ( target, options? ) {
	return _zoo(options).fetch_source( target, options );
}

function load_distribution ( target, options? ) {
	return _zoo(options).load_distribution( target, options );
}

function dependency_roots ( options? ) {
	return _zoo(options).dependency_roots(options);
}

function find_dependency ( module_name, min_version?, options? ) {
	return _zoo(options).find_dependency(
		module_name,
		min_version,
		options,
	);
}

function plan_install ( targets, options? ) {
	return _zoo(options).plan_install( targets, options );
}

function plan_remove ( targets, options? ) {
	return _zoo(options).plan_remove( targets, options );
}

function verify ( targets, options? ) {
	return _zoo(options).verify( targets, options );
}

function latest ( module_name, options? ) {
	return _zoo(options).latest( module_name, options );
}

function can_upgrade ( module_name, options? ) {
	return _zoo(options).can_upgrade( module_name, options );
}

function install ( targets, options? ) {
	return _zoo(options).install( targets, options );
}

function remove ( targets, options? ) {
	return _zoo(options).remove( targets, options );
}

function run_distribution_tests ( install_action, options? ) {
	return _zoo(options).run_distribution_tests( install_action, options );
}

function execute_removal ( removal_action, options? ) {
	return _zoo(options).execute_removal( removal_action, options );
}

function format_install_plan ( plan, options? ) {
	return _zoo(options).format_install_plan(plan);
}

function format_remove_plan ( plan, options? ) {
	return _zoo(options).format_remove_plan(plan);
}
