=encoding utf8
=head1 NAME
zdf/zuzubox - Author, verify, build, and upload ZDF-1 distributions.
=head1 SYNOPSIS
from zdf/zuzubox import Zuzubox;
let box := new Zuzubox();
let result := box.verify( "." );
=head1 DESCRIPTION
C<zdf/zuzubox> implements the reusable engine behind the C<zuzubox>
command-line tool. It mirrors the ZDF-1 rules enforced by the Zuzulang.org
upload validator, runs package tests in place, builds C<tar.gz> archives,
and uploads archives through the token-authenticated distribution API.
=head1 EXPORTS
=head2 C<Zuzubox>
Stateful helper class. Public methods are C<mint>, C<verify>, C<test>,
C<build>, and C<upload>.
=head2 C<run_cli(argv)>
Runs the command-line interface and returns a process exit code.
=head1 COPYRIGHT AND LICENCE
B<< zdf/zuzubox >> 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/archive import Archive;
from std/data/json import JSON;
from std/getopt import Getopt;
from std/io import Path, STDERR, STDOUT;
from std/net/http import UserAgent;
from std/proc import Env, Proc;
from std/string import chomp, chr, contains, ends_with, join, split, starts_with, substr, trim;
from std/time import Time;
from std/tui import colour_text, readline, supports_ansi;
from licence/spdx import is_spdx_expression, licence_expression_text, normalize_spdx_expression;
from test/parser import parse as parse_tap;
const ZUZUBOX_VERSION := "0.0.1";
const DEFAULT_MAX_BYTES := 20 * 1024 * 1024;
const DEFAULT_LICENCE := "Artistic-1.0 OR GPL-2.0-or-later";
function _has_zuzu_shebang;
function _archive_identity;
function _opt ( options, key, fallback := null ) {
if ( options instanceof Dict and options.exists(key) ) {
return options.get(key);
}
return fallback;
}
function _truthy ( value ) {
return value ? true : false;
}
function _has ( options, key ) {
return _truthy(_opt( options, key, false ));
}
function _copy_options ( options ) {
let out := {};
if ( options instanceof Dict ) {
for ( let key in options.keys() ) {
out.set( key, options.get(key) );
}
}
return out;
}
function _json_codec () {
return new JSON( pretty: true, canonical: true );
}
function _plain_json ( value ) {
return _json_codec().encode(value) _ "\n";
}
function _today () {
return ( new Time() ).strftime("%Y-%m-%d");
}
function _ensure_dir ( path ) {
return true if path.is_dir();
die path.to_String() _ " exists and is not a directory" if path.is_file();
let parent := path.parent();
_ensure_dir(parent) if not parent.is_dir();
path.mkdir();
return true;
}
function _ensure_parent ( path ) {
_ensure_dir(path.parent());
return true;
}
function _path_child ( root, String relative ) {
let out := root;
for ( let part in split( relative, "/" ) ) {
next if part eq "";
out := out.child(part);
}
return out;
}
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 _sorted_strings ( values ) {
return values.sort( fn ( a, b ) -> ( "" _ a ) cmp ( "" _ b ) );
}
function _walk_files ( root ) {
let found := [];
function walk ( path ) {
return if not path.is_dir();
for ( let child in path.children() ) {
if ( child.is_dir() ) {
walk(child);
}
else if ( child.is_file() ) {
found.push(_relative_path( root, child ));
}
}
}
walk(root);
return _sorted_strings(found);
}
function _walk_tests ( root ) {
let test_root := root.child("tests");
return [] if not test_root.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 rel := _relative_path( root, child );
if ( ends_with( rel, ".zzs" ) or _has_zuzu_shebang(child) ) {
found.push(rel);
}
}
}
}
walk(test_root);
return _sorted_strings(found);
}
function _safe_path ( String relative ) {
return false if relative eq "";
return false if starts_with( relative, "/" );
return false if contains( relative, "\\" );
return false if contains( relative, chr(0) );
for ( let part in split( relative, "/" ) ) {
return false if part eq "" or part eq "." or part eq "..";
}
return true;
}
function _module_name_ok ( String name ) {
return name ~ /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/;
}
function _dist_name_ok ( String name ) {
return name ~ /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
}
function _version_ok ( String version ) {
return version ~ /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
}
function _url_ok ( String url ) {
return url ~ /^https?:\/\/[^\s]+$/ and length url <= 1024;
}
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 := lines.length() == 0 ? "" : lines[0];
return starts_with( first, "#!" ) and contains( first, "zuzu" );
}
function _top_level_doc ( String relative ) {
return false if contains( relative, "/" );
return true if relative ~ /^[^\/]+\.(?:md|pod|txt)$/i;
return true if relative ~ /^[A-Z0-9_-]+$/;
return false;
}
function _zdf_allowed ( root, String relative ) {
return false if not _safe_path(relative);
return true if relative eq "zuzu-distribution.json";
return true if relative eq "Build.zzs";
return true if relative eq "LICENCE" or relative eq "LICENCE.txt";
return true if relative eq "LICENSE" or relative eq "LICENSE.txt";
return true if _top_level_doc(relative);
return true if relative ~ /^modules\/[A-Za-z0-9._\/-]+\.zzm$/;
return true if relative ~ /^scripts\/[A-Za-z0-9._\/-]+\.zzs$/;
if ( relative ~ /^scripts\/[A-Za-z0-9._\/-]+$/ ) {
let base := _path_child( root, relative ).basename();
return true if not _has_extension(base)
and _has_zuzu_shebang(_path_child( root, relative ));
}
return true if relative ~ /^tests\/[A-Za-z0-9._\/-]+$/;
return true if relative ~ /^inc\/[A-Za-z0-9._\/-]+\.zzm$/;
return false;
}
function _zdf_allowed_archive_entry ( String relative, data ) {
return false if not _safe_path(relative);
return true if relative eq "zuzu-distribution.json";
return true if relative eq "Build.zzs";
return true if relative eq "LICENCE" or relative eq "LICENCE.txt";
return true if relative eq "LICENSE" or relative eq "LICENSE.txt";
return true if _top_level_doc(relative);
return true if relative ~ /^modules\/[A-Za-z0-9._\/-]+\.zzm$/;
return true if relative ~ /^scripts\/[A-Za-z0-9._\/-]+\.zzs$/;
if ( relative ~ /^scripts\/[A-Za-z0-9._\/-]+$/ ) {
let base_parts := split( relative, "/" );
let base := base_parts[base_parts.length() - 1];
if ( not _has_extension(base) ) {
let text := to_string(data);
let lines := split( text, "\n" );
let first := lines.length() == 0 ? "" : lines[0];
return true if starts_with( first, "#!" ) and contains( first, "zuzu" );
}
}
return true if relative ~ /^tests\/[A-Za-z0-9._\/-]+$/;
return true if relative ~ /^inc\/[A-Za-z0-9._\/-]+\.zzm$/;
return false;
}
function _is_always_excluded ( String relative ) {
return true if relative eq ".zuzuboxignore";
return true if relative eq ".gitignore" or relative eq ".gitmodules";
return true if relative eq ".DS_Store";
return true if starts_with( relative, ".git/" );
return true if starts_with( relative, ".svn/" );
return true if starts_with( relative, ".hg/" );
return true if ends_with( relative, ".tar.gz" );
return true if ends_with( relative, ".swp" );
return true if ends_with( relative, "~" );
return false;
}
function _ignore_lines ( root ) {
let file := root.child(".zuzuboxignore");
return [] if not file.is_file();
let out := [];
for ( let line in split( file.slurp_utf8(), "\n" ) ) {
out.push(chomp(line));
}
return out;
}
function _ignore_rule_warnings ( root ) {
let warnings := [];
let number := 0;
for ( let raw in _ignore_lines(root) ) {
number++;
let line := trim(raw);
next if line eq "" or starts_with( line, "#" );
let negated := starts_with( line, "!" );
let rule := negated ? substr( line, 1 ) : line;
if ( trim(rule) eq "" ) {
warnings.push("line " _ number _ ": empty ignore rule");
next;
}
if ( contains( rule, "\\" ) ) {
warnings.push("line " _ number _ ": use / in ignore rules");
}
if (
rule eq ".."
or starts_with( rule, "../" )
or contains( rule, "/../" )
or ends_with( rule, "/.." )
) {
warnings.push("line " _ number _ ": ignore rule must stay inside the distribution");
}
}
return warnings;
}
function _rule_to_regexp_text ( String rule ) {
let out := "^";
let i := 0;
while ( i < length rule ) {
let ch := substr( rule, i, 1 );
if ( ch eq "*" ) {
if ( i + 1 < length rule and substr( rule, i + 1, 1 ) eq "*" ) {
out := out _ ".*";
i += 2;
next;
}
out := out _ "[^/]*";
}
else if ( ch eq "?" ) {
out := out _ "[^/]";
}
else if ( ch ~ /[A-Za-z0-9_\/-]/ ) {
out := out _ ch;
}
else if ( ch eq "." ) {
out := out _ "\\.";
}
else {
out := out _ "\\" _ ch;
}
i++;
}
return out _ "$";
}
function _join_array_from ( String separator, parts, start ) {
let out := [];
let i := start;
while ( i < parts.length() ) {
out.push(parts[i]);
i++;
}
return join( separator, out );
}
function _rule_matches ( String rule, String relative ) {
let anchored := starts_with( rule, "/" );
let dir_rule := ends_with( rule, "/" );
let pattern := rule;
pattern := substr( pattern, 1 ) if anchored;
pattern := substr( pattern, 0, length pattern - 1 ) if dir_rule;
return false if pattern eq "";
let candidates := [ relative ];
if ( not anchored ) {
let parts := split( relative, "/" );
let i := 1;
while ( i < parts.length() ) {
candidates.push(_join_array_from( "/", parts, i ));
i++;
}
}
let rx := _rule_to_regexp_text(pattern);
for ( let candidate in candidates ) {
return true if candidate ~ rx;
if ( dir_rule and starts_with( candidate, pattern _ "/" ) ) {
return true;
}
}
return false;
}
function _ignore_decision ( rules, String relative ) {
return {
excluded: true,
reason: "built-in exclusion",
} if _is_always_excluded(relative);
let excluded := false;
let reason := null;
for ( let raw in rules ) {
let line := trim(raw);
next if line eq "" or starts_with( line, "#" );
let negated := starts_with( line, "!" );
let rule := negated ? substr( line, 1 ) : line;
next if rule eq "";
if ( _rule_matches( rule, relative ) ) {
excluded := not negated;
reason := line;
}
}
return {
excluded: excluded,
reason: reason,
};
}
function _package_plan ( root ) {
let rules := _ignore_lines(root);
let included := [];
let excluded := [];
let disallowed := [];
for ( let relative in _walk_files(root) ) {
let decision := _ignore_decision( rules, relative );
if ( decision{excluded} ) {
excluded.push({
path: relative,
reason: decision.get( "reason", "ignored" ),
});
next;
}
if ( _zdf_allowed( root, relative ) ) {
included.push(relative);
}
else {
disallowed.push(relative);
}
}
return {
files: _sorted_strings(included),
excluded: excluded,
disallowed: _sorted_strings(disallowed),
};
}
function _module_from_source ( String relative ) {
let name := substr( relative, length "modules/" );
return substr( name, 0, length name - 4 );
}
function _script_from_source ( String relative ) {
let name := substr( relative, length "scripts/" );
if ( ends_with( lc(name), ".zzs" ) ) {
return substr( name, 0, length name - 4 );
}
return name;
}
function _trim_slashes ( String text ) {
let out := text;
while ( starts_with( out, "/" ) ) {
out := substr( out, 1 );
}
while ( ends_with( out, "/" ) ) {
out := substr( out, 0, length out - 1 );
}
return out;
}
function _reserved_name ( String name ) {
let lowered := lc(_trim_slashes(name));
for ( let reserved in [
"javascript",
"local",
"perl",
"std",
"test/more",
"test/parser",
"zuzu",
] ) {
return true if lowered eq reserved;
return true if starts_with( lowered, reserved _ "/" );
}
return false;
}
function _marker ( String status, options ) {
let unicode := not _has( options, "ascii" );
if ( unicode ) {
return "✅" if status eq "pass";
return "🟡" if status eq "warn";
return "❌" if status eq "fail";
}
return "[okay]" if status eq "pass";
return "[warn]" if status eq "warn";
return "[FAIL]";
}
function _colour_for ( String status ) {
return "green" if status eq "pass";
return "yellow" if status eq "warn";
return "red" if status eq "fail";
return null;
}
function _colour_enabled ( options ) {
return false if _has( options, "json" );
return false if _has( options, "no-colour" ) or _has( options, "no-color" );
let when := "" _ _opt( options, "colour", _opt( options, "color", "auto" ) );
return false if when eq "never";
return true if when eq "always";
return false if Env.get("NO_COLOR", "") ne "";
return supports_ansi();
}
function _paint ( text, colour, options ) {
return "" _ text if not _colour_enabled(options);
return colour_text( text, colour );
}
function _status_line ( check, options ) {
return if _has( options, "quiet" );
let status := check{status};
STDOUT.say(
_paint( _marker( status, options ), _colour_for(status), options ) _
" " _
check{message}
);
}
function _summary_text ( String command, summary, extra? ) {
let text := command _ ": " _
summary{passed} _ " passed, " _
summary{warnings} _ " warnings, " _
summary{failures} _ " failures";
if ( extra != null and extra ne "" ) {
text := text _ " (" _ extra _ ")";
}
return text;
}
function _summarize_checks ( checks ) {
let summary := {
passed: 0,
warnings: 0,
failures: 0,
};
for ( let check in checks ) {
if ( check{status} eq "pass" ) {
summary{passed}++;
}
else if ( check{status} eq "warn" ) {
summary{warnings}++;
}
else {
summary{failures}++;
}
}
return summary;
}
function _check ( checks, String id, String status, String message ) {
checks.push({
id: id,
status: status,
message: message,
});
return checks;
}
function _read_metadata ( root ) {
return ( new JSON( pairlists: true ) ).load(root.child("zuzu-distribution.json"));
}
function _metadata_identifier ( metadata ) {
return "" _ metadata{name} _ "-" _ metadata{version};
}
function _zuzu_command () {
let configured := Env.get("ZUZU_COMMAND", "");
return configured if configured ne "";
for ( let candidate in [
"../../zuzu-perl/bin/zuzu",
"../zuzu-perl/bin/zuzu",
"zuzu-perl/bin/zuzu",
"bin/zuzu",
] ) {
let path := new Path(candidate);
return path.absolute().to_String()
if path.is_file();
}
return "zuzu";
}
function _find_working_copy_for_archive ( archive ) {
let basename := archive.basename();
return null if not ends_with( basename, ".tar.gz" );
let identifier := substr( basename, 0, length basename - 7 );
let parent := archive.parent();
for ( let child in parent.children() ) {
next if not child.is_dir();
let meta_file := child.child("zuzu-distribution.json");
next if not meta_file.is_file();
try {
let meta := ( new JSON( pairlists: true ) ).load(meta_file);
if ( _metadata_identifier(meta) eq identifier ) {
return child;
}
}
catch {
next;
}
}
return null;
}
function _include_args ( root ) {
let args := [];
let modules := root.child("modules");
let inc := root.child("inc");
args.push("-I" _ modules.to_String()) if modules.is_dir();
args.push("-I" _ inc.to_String()) if inc.is_dir();
return args;
}
function _test_ok ( parsed, run_result ) {
return false if not Proc.is_success(run_result);
return false if parsed{assertions}{failed} > 0;
return false if parsed{planned} != null
and parsed{assertions}{total} != parsed{planned};
return true;
}
function _root_for ( dir ) {
let path := new Path("" _ dir);
return path.absolute();
}
function _size_human ( Number bytes ) {
return "" _ bytes _ " B" if bytes < 1024;
let kib := bytes / 1024;
return "" _ int(kib * 10) / 10 _ " KiB" if bytes < 1024 * 1024;
let mib := bytes / ( 1024 * 1024 );
return "" _ int(mib * 10) / 10 _ " MiB";
}
function _archive_identity ( archive ) {
let loaded := Archive.load(archive, "tar.gz");
let root := null;
let metadata := null;
let shippable := false;
for ( let entry in loaded{entries} ) {
die "archive contains an unsafe path" if not _safe_path(entry{path});
let parts := split( entry{path}, "/" );
die "archive contains an unsafe path" if parts.length() < 2;
if ( root == null ) {
root := parts[0];
}
else if ( root ne parts[0] ) {
die "archive contains multiple top-level directories";
}
let relative := _join_array_from( "/", parts, 1 );
if ( relative eq "zuzu-distribution.json" ) {
metadata := ( new JSON( pairlists: true ) ).decode_binarystring(
entry{data},
);
}
if ( starts_with( relative, "modules/" ) ) {
let module_name := _module_from_source(relative);
die "archive contains a reserved module name"
if _reserved_name(module_name);
shippable := true;
}
else if ( starts_with( relative, "scripts/" ) ) {
let script_name := _script_from_source(relative);
die "archive contains a reserved script name"
if _reserved_name(script_name);
shippable := true;
}
die "archive contains an unexpected file"
if not _zdf_allowed_archive_entry(
relative,
entry{data},
);
}
die "archive is missing zuzu-distribution.json" if metadata == null;
for ( let field in [ "name", "version", "author", "license" ] ) {
die "archive metadata is missing " _ field
if not metadata.exists(field)
or not( metadata.get(field) instanceof String )
or trim(metadata.get(field)) eq "";
}
die "archive metadata name is not valid"
if not _dist_name_ok("" _ metadata{name});
die "archive metadata version is not valid"
if not _version_ok("" _ metadata{version});
if ( metadata.exists("status") ) {
let status := "" _ metadata{status};
die "archive metadata status is not valid"
if status ne "stable" and status ne "trial";
}
die "archive metadata repo is not valid"
if metadata.exists("repo") and not _url_ok("" _ metadata{repo});
if ( metadata.exists("dependencies") ) {
die "archive metadata dependencies must be an object"
if not( metadata{dependencies} instanceof Dict )
and not( metadata{dependencies} instanceof PairList );
for ( let dep in metadata{dependencies}.keys() ) {
let value := metadata{dependencies}.get(dep);
die "archive metadata dependencies are not valid"
if not _module_name_ok("" _ dep)
or not( value instanceof String )
or trim(value) eq "";
}
}
die "archive contains no shippable module or script" if not shippable;
let identifier := _metadata_identifier(metadata);
die "archive identity does not match metadata" if identifier ne root;
return {
identifier: identifier,
metadata: metadata,
root: root,
};
}
function _confirmed_upload ( archive, String target, identity, options ) {
return true if _has( options, "yes" );
if ( _has( options, "json" ) ) {
return false;
}
STDERR.say("Upload " _ identity{identifier} _ " to " _ target _ "?");
let answer := readline( "Proceed? [y/N] ", "n", null );
return false if answer == null;
return ( "" _ answer ) ~ /^(?:y|yes)$/i;
}
function _upload_progress ( String message, options ) {
return true if _has( options, "quiet" ) or _has( options, "json" );
STDERR.say(message);
return true;
}
function _multipart_archive_body ( archive, String boundary ) {
let head := "--" _ boundary _ "\r\n"
_ "Content-Disposition: form-data; name=\"archive\"; filename=\""
_ archive.basename() _ "\"\r\n"
_ "Content-Type: application/gzip\r\n\r\n";
let tail := "\r\n--" _ boundary _ "--\r\n";
return to_binary(head) _ archive.slurp() _ to_binary(tail);
}
function _multipart_archive_file ( archive ) {
let boundary := "zuzubox-" _ ( new Time() ).epoch() _ "-" _ archive.size();
let body := Path.tempfile();
body.spew(_multipart_archive_body( archive, boundary ));
return {
path: body,
boundary: boundary,
};
}
function _licence_suggestions () {
return join( ", ", [
DEFAULT_LICENCE,
"GPL2+",
"GPL3+",
"LGPL2.1+",
"MIT",
"Apache",
"CC0",
"BSD-3-clause",
] );
}
function _licence_alias ( String raw ) {
let trimmed := trim(raw);
return DEFAULT_LICENCE if trimmed eq "";
if ( trimmed eq "GPL2+" ) { return "GPL-2.0-or-later"; }
if ( trimmed eq "GPL3+" ) { return "GPL-3.0-or-later"; }
if ( trimmed eq "LGPL2.1+" ) { return "LGPL-2.1-or-later"; }
if ( trimmed eq "Apache" ) { return "Apache-2.0"; }
if ( trimmed eq "CC0" ) { return "CC0-1.0"; }
if ( trimmed eq "BSD-3-clause" ) { return "BSD-3-Clause"; }
return trimmed;
}
function _normalize_licence_choice ( value ) {
return normalize_spdx_expression(_licence_alias("" _ value));
}
function _mint_licence ( options, Boolean prompted ) {
let raw := _opt( options, "licence", _opt( options, "license", null ) );
if ( raw != null ) {
let spdx := _normalize_licence_choice(raw);
die "licence must be an SPDX expression; suggestions: "
_ _licence_suggestions()
if spdx == null;
return spdx;
}
return DEFAULT_LICENCE if not prompted;
STDERR.say("Suggested licence choices: " _ _licence_suggestions());
STDERR.say("You can also enter any valid SPDX licence expression.");
STDERR.say("Press Enter for " _ DEFAULT_LICENCE _ ".");
while ( true ) {
raw := readline( "Licence: ", DEFAULT_LICENCE, null );
die "mint requires a licence" if raw == null;
let spdx := _normalize_licence_choice(raw);
return spdx if spdx != null;
STDERR.say(
"Enter a valid SPDX expression. Suggestions: "
_ _licence_suggestions()
);
}
}
function _licence_file_text ( String licence, options? ) {
return "Add the full licence text for: " _ licence _ "\n"
if _has( options, "no-licence-text" )
or _has( options, "no-license-text" );
try {
let text := licence_expression_text(licence, options);
return text if text != null and trim(text) ne "";
}
catch {
}
return "Add the full licence text for: " _ licence _ "\n";
}
class Zuzubox {
let zuzu_command with get, set := null;
method _zuzu_command () {
return self{zuzu_command} if self{zuzu_command} != null;
self{zuzu_command} := _zuzu_command();
return self{zuzu_command};
}
method mint ( dir, options? ) {
let root_name := "" _ ( dir == null ? "" : dir );
let module_name := _opt( options, "module", null );
let prompted := false;
while ( module_name == null or module_name eq "" ) {
prompted := true;
module_name := readline( "Primary module name: ", "", null );
die "mint requires a primary module name" if module_name == null;
module_name := trim(module_name);
}
die "primary module name is not valid" if not _module_name_ok(module_name);
let default_name := module_name;
default_name := join( "-", split( default_name, "/" ) );
let dist_name := _opt( options, "name", null );
if ( dist_name == null or dist_name eq "" ) {
prompted := true;
dist_name := readline(
"Distribution name [" _ default_name _ "]: ",
default_name,
null,
);
dist_name := default_name if dist_name == null or dist_name eq "";
}
dist_name := trim(dist_name);
die "distribution name is not valid" if not _dist_name_ok(dist_name);
let licence := _mint_licence( options, prompted );
let root := new Path( root_name eq "" ? dist_name : root_name );
if ( root.is_file() ) {
die "mint target is not a directory";
}
if ( root.is_dir() ) {
die "mint target is not empty"
if root.children().length() > 0;
}
else {
_ensure_dir(root);
}
let author := Env.get("ZUZU_AUTHOR", Env.get("USER", "Your Name"));
let codec := _json_codec();
let created := [];
function write_file ( String relative, String text ) {
let file := _path_child( root, relative );
die `refusing to overwrite ${relative}`
if file.is_file() or file.is_dir();
_ensure_parent(file);
file.spew_utf8(text);
created.push(relative);
}
write_file(
"zuzu-distribution.json",
codec.encode({
name: dist_name,
version: "0.0.1",
author: author,
license: licence,
status: "trial",
abstract: "",
dependencies: {},
}) _ "\n",
);
write_file(
"modules/" _ module_name _ ".zzm",
"=encoding utf8\n\n" _
"=head1 NAME\n\n" _
module_name _ " - Newly minted ZuzuScript module.\n\n" _
"=head1 DESCRIPTION\n\n" _
"Replace this stub with useful documentation and code.\n\n" _
"=cut\n",
);
write_file(
"tests/" _ module_name _ ".zzs",
"from " _ module_name _ " import *;\n\n" _
"from test/more import *;\n\n" _
"pass( \"" _ module_name _ " loads\" );\n\n" _
"done_testing();\n",
);
write_file(
"CHANGELOG.md",
"# Changelog\n\n" _
"## 0.0.1 - " _ _today() _ "\n\n" _
"*First release.*\n",
);
write_file(
"README.md",
"# " _ dist_name _ "\n\n" _
"`" _ module_name _ "` is a ZuzuScript module.\n\n" _
"## Synopsis\n\n```zzs\n" _
"from " _ module_name _ " import *;\n" _
"```\n\n## Description\n\nDescribe the distribution here.\n",
);
write_file(
"LICENCE",
_licence_file_text( licence, options ),
);
write_file(
".zuzuboxignore",
"# Files excluded from ZDF-1 archives.\n" _
"# scratch/\n# *.bak\n",
);
return {
ok: true,
dir: root.to_String(),
files: _sorted_strings(created),
name: dist_name,
module: module_name,
};
}
method verify ( dir, options? ) {
let root := _root_for(dir);
let checks := [];
let meta := null;
let metadata_file := root.child("zuzu-distribution.json");
if ( metadata_file.is_file() ) {
try {
meta := _read_metadata(root);
if ( meta instanceof Dict or meta instanceof PairList ) {
_check( checks, "metadata-json", "pass", "zuzu-distribution.json is valid JSON" );
}
else {
_check( checks, "metadata-json", "fail", "zuzu-distribution.json must contain an object" );
}
}
catch ( Exception e ) {
_check( checks, "metadata-json", "fail", "zuzu-distribution.json does not parse: " _ e{message} );
}
}
else {
_check( checks, "metadata-exists", "fail", "zuzu-distribution.json is missing" );
}
if ( meta instanceof Dict or meta instanceof PairList ) {
for ( let field in [ "name", "version", "author", "license" ] ) {
let ok := meta.exists(field)
and meta.get(field) instanceof String
and trim(meta.get(field)) ne "";
_check(
checks,
"metadata-" _ field,
ok ? "pass" : "fail",
ok ? field _ " is present" : field _ " is missing or empty",
);
}
if ( meta.exists("license") ) {
_check(
checks,
"metadata-licence-spdx",
is_spdx_expression(meta.get("license")) ? "pass" : "fail",
is_spdx_expression(meta.get("license"))
? "licence is a valid SPDX expression"
: "licence is not a valid SPDX expression",
);
}
for ( let warning in _ignore_rule_warnings(root) ) {
_check( checks, "ignore-rule", "warn", ".zuzuboxignore " _ warning );
}
if ( meta.exists("name") ) {
_check(
checks,
"metadata-name-pattern",
_dist_name_ok("" _ meta{name}) ? "pass" : "fail",
"name matches ZDF-1 pattern",
);
}
if ( meta.exists("version") ) {
_check(
checks,
"metadata-version-pattern",
_version_ok("" _ meta{version}) ? "pass" : "fail",
"version matches ZDF-1 pattern",
);
}
if ( meta.exists("status") ) {
let status := "" _ meta{status};
_check(
checks,
"metadata-status",
( status eq "stable" or status eq "trial" ) ? "pass" : "fail",
"status is " _ status,
);
}
if ( meta.exists("repo") ) {
_check(
checks,
"metadata-repo",
_url_ok("" _ meta{repo}) ? "pass" : "fail",
"repo is a valid http/https URL",
);
}
if ( meta.exists("dependencies") ) {
let deps_ok := meta{dependencies} instanceof Dict
or meta{dependencies} instanceof PairList;
if ( deps_ok ) {
for ( let dep in meta{dependencies}.keys() ) {
let value := meta{dependencies}.get(dep);
deps_ok := false if not _module_name_ok("" _ dep);
deps_ok := false if not( value instanceof String ) or trim(value) eq "";
}
}
_check(
checks,
"metadata-dependencies",
deps_ok ? "pass" : "fail",
"dependencies are valid",
);
}
if ( meta.exists("abstract") ) {
let abstract_ok := meta{abstract} instanceof String
and length meta{abstract} <= 140;
_check(
checks,
"metadata-abstract",
abstract_ok ? "pass" : "warn",
abstract_ok ? "abstract is upload friendly" : "abstract should be a string of 140 characters or fewer",
);
}
else {
_check( checks, "metadata-abstract", "warn", "abstract is missing" );
}
}
let plan := _package_plan(root);
if ( plan{disallowed}.length() == 0 ) {
_check( checks, "package-membership", "pass", "all unignored files are ZDF-1 allowed" );
}
else {
for ( let bad in plan{disallowed} ) {
_check( checks, "package-membership", "fail", bad _ " is not a ZDF-1 allowed path" );
}
}
let shippable := false;
for ( let file in plan{files} ) {
shippable := true if starts_with( file, "modules/" )
or starts_with( file, "scripts/" );
}
_check(
checks,
"shippable-artifact",
shippable ? "pass" : "fail",
shippable ? "at least one module or script will ship" : "no modules or scripts will ship",
);
for ( let file in plan{files} ) {
if ( starts_with( file, "modules/" ) ) {
let module_name := _module_from_source(file);
_check(
checks,
"reserved-" _ file,
_reserved_name(module_name) ? "fail" : "pass",
_reserved_name(module_name) ? file _ " uses a reserved module name" : file _ " uses an uploadable module name",
);
}
else if ( starts_with( file, "scripts/" ) ) {
let script_name := _script_from_source(file);
_check(
checks,
"reserved-" _ file,
_reserved_name(script_name) ? "fail" : "pass",
_reserved_name(script_name) ? file _ " uses a reserved script name" : file _ " uses an uploadable script name",
);
}
}
for ( let file in plan{files} ) {
next if not starts_with( file, "modules/" )
and not starts_with( file, "scripts/" );
let argv := _include_args(root);
argv.push("--lint");
argv.push(file);
let lint := Proc.run(
self._zuzu_command(),
argv,
{
cwd: root.to_String(),
capture_stdout: true,
capture_stderr: true,
},
);
if ( not Proc.is_success(lint) and starts_with( file, "modules/" ) ) {
let module_name := _module_from_source(file);
let fallback_argv := _include_args(root);
fallback_argv.push("-e");
fallback_argv.push("from " _ module_name _ " import *;");
lint := Proc.run(
self._zuzu_command(),
fallback_argv,
{
cwd: root.to_String(),
capture_stdout: true,
capture_stderr: true,
},
);
}
_check(
checks,
"parse-" _ file,
Proc.is_success(lint) ? "pass" : "fail",
Proc.is_success(lint)
? file _ " parses"
: file _ " does not parse: " _ Proc.status_text(lint),
);
}
let docs := false;
let changelog := false;
let licence := false;
for ( let file in plan{files} ) {
docs := true if file eq "README.md" or file eq "README.txt" or file eq "README.pod";
changelog := true if file ~ /^CHANGELOG(?:\.(?:md|txt|pod))?$/i;
licence := true if file eq "LICENCE" or file eq "LICENCE.txt" or file eq "LICENSE" or file eq "LICENSE.txt";
}
_check( checks, "readme", docs ? "pass" : "warn", docs ? "README exists" : "README is missing" );
_check(
checks,
"changelog",
changelog ? "pass" : "warn",
changelog ? "CHANGELOG exists" : "CHANGELOG is missing",
);
if ( changelog and meta != null and meta.exists("version") ) {
let text := "";
for ( let name in [ "CHANGELOG.md", "CHANGELOG.txt", "CHANGELOG.pod", "CHANGELOG" ] ) {
let file := root.child(name);
text := file.slurp_utf8() if file.is_file();
}
_check(
checks,
"changelog-version",
contains( text, "" _ meta{version} ) ? "pass" : "warn",
contains( text, "" _ meta{version} ) ? "CHANGELOG mentions current version" : "CHANGELOG does not mention current version",
);
}
_check( checks, "licence", licence ? "pass" : "warn", licence ? "licence file exists" : "licence file is missing" );
let tests := _walk_tests(root);
_check(
checks,
"tests-present",
tests.length() > 0 ? "pass" : "warn",
tests.length() > 0 ? "tests are present" : "no tests found",
);
let total_size := 0;
for ( let file in plan{files} ) {
total_size += _path_child( root, file ).size();
}
let max_bytes := _opt( options, "max-bytes", DEFAULT_MAX_BYTES );
_check(
checks,
"package-size",
total_size <= max_bytes ? "pass" : "warn",
"package candidate is " _ _size_human(total_size),
);
let summary := _summarize_checks(checks);
let strict := _has( options, "strict" );
if ( strict and summary{warnings} > 0 ) {
summary{failures} += summary{warnings};
}
return {
ok: summary{failures} == 0,
checks: checks,
summary: summary,
files: plan{files},
excluded: plan{excluded},
disallowed: plan{disallowed},
};
}
method test ( dir, options? ) {
let root := _root_for(dir);
let tests := _walk_tests(root);
let results := [];
let extra := _opt( options, "extra_args", [] );
for ( let test in tests ) {
let argv := _include_args(root);
for ( let arg in extra ) {
argv.push(arg);
}
argv.push(test);
let run_result := Proc.run(
self._zuzu_command(),
argv,
{
cwd: root.to_String(),
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),
stdout: run_result{stdout},
stderr: run_result{stderr},
tap: parsed,
});
}
let ok := true;
for ( let result in results ) {
ok := false if not result{ok};
}
return {
ok: ok,
tests: results,
count: tests.length(),
};
}
method build ( dir, options? ) {
let root := _root_for(dir);
let warnings := [];
let verify_result := null;
if ( not _has( options, "skip-verify" ) ) {
verify_result := self.verify( root.to_String(), options );
if ( not verify_result{ok} ) {
return {
ok: false,
error: "verify failed",
verify: verify_result,
};
}
}
else {
warnings.push("built without running verify");
}
let test_result := null;
if ( not _has( options, "no-test" ) ) {
test_result := self.test( root.to_String(), options );
if ( not test_result{ok} ) {
return {
ok: false,
error: "tests failed",
verify: verify_result,
tests: test_result,
};
}
}
else {
warnings.push("built without running tests");
}
let meta := _read_metadata(root);
let identifier := _metadata_identifier(meta);
let plan := _package_plan(root);
let output := _opt( options, "output", identifier _ ".tar.gz" );
let out_path := new Path(output);
if (
( out_path.is_file() or out_path.is_dir() )
and not _has( options, "force" )
and not _has( options, "dry-run" )
) {
return {
ok: false,
error: "archive already exists",
archive: out_path.to_String(),
};
}
let archive_entries := [];
for ( let relative in plan{files} ) {
archive_entries.push({
path: identifier _ "/" _ relative,
data_from: _path_child( root, relative ),
});
}
if ( _has( options, "dry-run" ) ) {
return {
ok: true,
dry_run: true,
identifier: identifier,
archive: out_path.to_String(),
file_count: archive_entries.length(),
files: plan{files},
excluded: plan{excluded},
warnings: warnings,
};
}
Archive.dump(
out_path,
{ entries: archive_entries },
"tar.gz",
);
return {
ok: true,
identifier: identifier,
archive: out_path.to_String(),
size_bytes: out_path.size(),
file_count: archive_entries.length(),
files: plan{files},
excluded: plan{excluded},
warnings: warnings,
verify: verify_result,
tests: test_result,
};
}
method upload ( archive_path, options? ) {
let archive := new Path("" _ archive_path);
if ( not archive.is_file() ) {
return { ok: false, error: "archive not found" };
}
let identity := null;
try {
identity := _archive_identity(archive);
}
catch ( Exception e ) {
return {
ok: false,
error: "archive precheck failed: " _ e{message},
};
}
let work_dir := _find_working_copy_for_archive(archive);
if ( work_dir != null and not _has( options, "no-precheck" ) ) {
let verify_result := self.verify( work_dir.to_String(), options );
if ( not verify_result{ok} ) {
return {
ok: false,
error: "working copy verify failed",
verify: verify_result,
};
}
if ( not _has( options, "no-test" ) ) {
let test_result := self.test( work_dir.to_String(), options );
if ( not test_result{ok} ) {
return {
ok: false,
error: "working copy tests failed",
tests: test_result,
};
}
}
}
let base_url := _opt( options, "base-url", Env.get( "ZUZUBOX_BASE_URL", "https://zuzulang.org" ) );
if ( not starts_with( base_url, "https://" ) and not _has( options, "insecure" ) ) {
return { ok: false, error: "refusing to send token to a non-HTTPS URL" };
}
let target := base_url _ "/api/v1/distributions";
if ( _has( options, "dry-run" ) ) {
return {
ok: true,
dry_run: true,
archive: archive.to_String(),
identifier: identity{identifier},
url: target,
message: "dry run; no upload token read and no request sent",
};
}
let token := Env.get("ZUZUBOX_UPLOAD_TOKEN", "");
if ( token eq "" and _opt( options, "token-file", null ) != null ) {
token := trim(( new Path(_opt( options, "token-file" )) ).slurp_utf8());
}
if ( token eq "" ) {
return {
ok: false,
auth_error: true,
error: "set ZUZUBOX_UPLOAD_TOKEN or pass --token-file",
};
}
if ( not _has( options, "yes" ) and _has( options, "json" ) ) {
return {
ok: false,
error: "upload confirmation required; pass --yes for non-interactive upload",
};
}
if ( not _confirmed_upload( archive, target, identity, options ) ) {
return {
ok: false,
error: "upload declined; pass --yes to skip the confirmation prompt",
};
}
_upload_progress(
"upload: sending " _ archive.basename() _ " (" _
_size_human(archive.size()) _ ") to " _ target,
options,
);
let multipart := _multipart_archive_file(archive);
let ua := new UserAgent( agent: "zuzubox/" _ ZUZUBOX_VERSION );
let req := ua
.build_request( "POST", target )
.auth_bearer(token)
.header(
"Content-Type",
"multipart/form-data; boundary=" _ multipart{boundary},
)
.upload_from(multipart{path}.to_String());
let response := null;
try {
response := req.send(ua);
}
catch ( Exception e ) {
try {
multipart{path}.remove();
}
catch {
}
return {
ok: false,
error: "network error while uploading: " _ e{message},
};
}
multipart{path}.remove();
_upload_progress(
"upload: server responded with HTTP " _ response.status(),
options,
);
let body := response.content();
let parsed := null;
try {
parsed := body instanceof BinaryString
? ( new JSON() ).decode_binarystring(body)
: ( new JSON() ).decode("" _ body);
}
catch {
parsed := {
ok: false,
error: "server returned non-JSON response",
};
}
parsed{status} := response.status();
if (
response.status() == 401
or parsed.get( "code", "" ) eq "auth_required"
or parsed.get( "code", "" ) eq "invalid_token"
) {
parsed{auth_error} := true;
}
return parsed;
}
}
function _usage ( command := null ) {
if ( command eq "mint" ) {
return join( "\n", [
"Usage: zuzubox mint [options] [DIR]",
"",
"Creates a new ZDF-1 distribution skeleton.",
"",
"Options:",
" --module NAME primary module name, such as colour/palette",
" --name NAME distribution name, such as colour-palette",
" --licence EXPR suggested name or any SPDX expression",
" --license EXPR alias for --licence",
" --no-licence-text write a placeholder LICENCE file",
] ) _ "\n";
}
if ( command eq "verify" ) {
return join( "\n", [
"Usage: zuzubox verify [options] [DIR]",
"",
"Checks metadata, package layout, parseability, and archive size.",
"",
"Options:",
" --json print a machine-readable report",
" --max-bytes N override the archive size limit",
" --quiet, -q suppress human progress output",
] ) _ "\n";
}
if ( command eq "test" ) {
return join( "\n", [
"Usage: zuzubox test [options] [DIR] [-- extra zuzu args]",
"",
"Runs distribution tests with the packaged modules and inc/ path.",
"",
"Options:",
" --zuzu PATH ZuzuScript command to run",
" --jobs N accepted for forward compatibility; default 1",
" --json print a machine-readable report",
] ) _ "\n";
}
if ( command eq "build" ) {
return join( "\n", [
"Usage: zuzubox build [options] [DIR]",
"",
"Verifies, tests, and writes a ZDF-1 tar.gz archive.",
"",
"Options:",
" --output PATH archive output path",
" --force overwrite an existing archive",
" --dry-run show the package plan without writing",
" --no-test skip tests after verification",
" --skip-verify skip verification",
] ) _ "\n";
}
if ( command eq "upload" ) {
return join( "\n", [
"Usage: zuzubox upload [options] [ARCHIVE]",
"",
"Uploads an archive using a Zuzulang.org upload token.",
"",
"Options:",
" --base-url URL server base URL",
" --token-file PATH read the bearer token from a file",
" --yes, -y skip confirmation prompts",
" --dry-run run checks without uploading",
" --no-precheck skip working-copy verification",
" --insecure allow a non-HTTPS base URL",
] ) _ "\n";
}
return join( "\n", [
"Usage:",
" zuzubox mint [options] [DIR]",
" zuzubox verify [options] [DIR]",
" zuzubox test [options] [DIR] [-- extra zuzu args]",
" zuzubox build [options] [DIR]",
" zuzubox upload [options] [ARCHIVE]",
" zuzubox help [COMMAND]",
" zuzubox version",
"",
"Global options:",
" --help, -h show help",
" --version show version",
" --quiet, -q suppress progress output",
" --json print JSON where supported",
" --colour WHEN auto, always, or never",
" --no-colour disable colour",
" --ascii use ASCII status markers",
" --yes, -y assume yes for prompts",
" --licence NAME suggested names or any SPDX expression for mint",
] ) _ "\n";
}
function _dir_arg ( args ) {
return args.length() == 0 ? "." : args[0];
}
function _upload_archive_arg ( args ) {
return args[0] if args.length() > 0;
let root := new Path(".");
let candidates := [];
for ( let child in root.children() ) {
if ( child.is_file() and ends_with( child.basename(), ".tar.gz" ) ) {
candidates.push(child);
}
}
if ( candidates.length() == 0 ) {
die "no .tar.gz archive found; pass ARCHIVE";
}
let preferred := [];
let metadata := null;
try {
metadata := _read_metadata(root);
}
catch {
metadata := null;
}
if ( metadata instanceof Dict or metadata instanceof PairList ) {
let expected := _metadata_identifier(metadata) _ ".tar.gz";
for ( let candidate in candidates ) {
preferred.push(candidate) if candidate.basename() eq expected;
}
}
if ( preferred.length() == 1 ) {
return preferred[0].to_String();
}
if ( preferred.length() > 1 ) {
die "multiple matching .tar.gz archives found; pass ARCHIVE";
}
if ( candidates.length() == 1 ) {
return candidates[0].to_String();
}
die "multiple .tar.gz archives found; pass ARCHIVE";
}
function _split_at_double_dash ( argv ) {
let before := [];
let after := [];
let seen := false;
for ( let arg in argv ) {
if ( not seen and "" _ arg eq "--" ) {
seen := true;
next;
}
if ( seen ) {
after.push(arg);
}
else {
before.push(arg);
}
}
return {
before: before,
after: after,
};
}
function _parse ( argv ) {
return Getopt.parse(
argv,
[
"help|h",
"version",
"quiet|q",
"verbose|v",
"json",
"colour=s",
"color=s",
"no-colour",
"no-color",
"ascii",
"yes|y",
"strict",
"max-bytes=i",
"jobs=i",
"no-test",
"skip-verify",
"dry-run",
"output|o=s",
"force",
"module=s",
"name=s",
"licence=s",
"license=s",
"token-file=s",
"base-url=s",
"insecure",
"no-precheck",
],
);
}
function _print_verify ( result, options ) {
if ( _has( options, "json" ) ) {
STDOUT.print(_plain_json(result));
return true;
}
for ( let check in result{checks} ) {
_status_line( check, options );
}
STDOUT.say(
_paint(
_summary_text( "verify", result{summary} ),
result{ok} ? "green" : "red",
options,
)
);
return true;
}
function _print_test ( result, options ) {
if ( _has( options, "json" ) ) {
STDOUT.print(_plain_json(result));
return true;
}
for ( let test in result{tests} ) {
if ( test{ok} ) {
STDOUT.say(_paint( _marker( "pass", options ), "green", options ) _ " " _ test{test})
if not _has( options, "quiet" );
}
else {
STDOUT.say(_paint( _marker( "fail", options ), "red", options ) _ " " _ test{test} _ " failed");
STDOUT.print(test{stdout}) if test{stdout} ne "";
STDERR.print(test{stderr}) if test{stderr} ne "";
}
}
let summary := {
passed: result{tests}.grep( fn r -> r{ok} ).length(),
warnings: 0,
failures: result{tests}.grep( fn r -> not r{ok} ).length(),
};
STDOUT.say(_paint( _summary_text( "test", summary ), result{ok} ? "green" : "red", options ));
return true;
}
function _print_build ( result, options ) {
if ( _has( options, "json" ) ) {
STDOUT.print(_plain_json(result));
return true;
}
if ( not result{ok} ) {
STDERR.say("build: " _ result{error});
return true;
}
if ( result.get( "dry_run", false ) ) {
STDOUT.say("build: dry run for " _ result{identifier});
}
else {
STDOUT.say(
_paint( "build: wrote " _ result{archive}, "green", options ) _
" (" _ _size_human(result{size_bytes}) _ ", " _
result{file_count} _ " files)"
);
}
if ( result.exists("warnings") ) {
for ( let warning in result{warnings} ) {
STDERR.say("build: warning: " _ warning);
}
}
if ( _has( options, "verbose" ) or result.get( "dry_run", false ) ) {
for ( let file in result{files} ) {
STDOUT.say(" + " _ file);
}
for ( let ignored in result{excluded} ) {
STDOUT.say(" - " _ ignored{path} _ " (" _ ignored{reason} _ ")");
}
}
return true;
}
function _print_mint ( result, options ) {
if ( _has( options, "json" ) ) {
STDOUT.print(_plain_json(result));
return true;
}
STDOUT.say(_paint( "mint: created " _ result{dir}, "green", options ));
for ( let file in result{files} ) {
STDOUT.say(" " _ _paint( file, "cyan", options ));
}
STDOUT.say("next: edit metadata, then run zuzubox verify");
return true;
}
function _print_upload ( result, options ) {
if ( _has( options, "json" ) ) {
STDOUT.print(_plain_json(result));
return true;
}
if ( result{ok} ) {
if ( result.get( "dry_run", false ) ) {
STDOUT.say("upload: dry run for " _ result{identifier} _ " to " _ result{url});
}
else {
STDOUT.say(_paint( "upload: accepted " _ result.get( "identifier", "" ), "green", options ));
STDOUT.say(result{url}) if result.exists("url");
}
}
else {
STDERR.say("upload: " _ result.get( "error", "failed" ));
}
return true;
}
function _tail ( values ) {
let out := [];
let i := 1;
while ( i < values.length() ) {
out.push(values[i]);
i++;
}
return out;
}
function run_cli ( argv ) {
let split_argv := _split_at_double_dash(argv);
let parsed := _parse(split_argv{before});
if ( not parsed{ok} ) {
STDERR.say(parsed{error});
STDERR.print(_usage());
return 2;
}
let options := parsed{options};
let args := parsed{argv};
if ( _has( options, "version" ) ) {
STDOUT.say("zuzubox " _ ZUZUBOX_VERSION);
return 0;
}
if ( _has( options, "help" ) or args.length() == 0 or args[0] eq "help" ) {
let help_command := null;
if ( args.length() > 0 ) {
help_command := args[0] eq "help"
? ( args.length() > 1 ? args[1] : null )
: args[0];
}
STDOUT.print(_usage(help_command));
return 0;
}
let command := args[0];
let rest := _tail(args);
let box := new Zuzubox();
try {
if ( command eq "mint" ) {
let result := box.mint( rest.length() == 0 ? null : rest[0], options );
_print_mint( result, options );
return 0;
}
if ( command eq "verify" ) {
let result := box.verify( _dir_arg(rest), options );
_print_verify( result, options );
return result{ok} ? 0 : 1;
}
if ( command eq "test" ) {
options.set( "extra_args", split_argv{after} );
let result := box.test( _dir_arg(rest), options );
_print_test( result, options );
return result{ok} ? 0 : 1;
}
if ( command eq "build" ) {
let result := box.build( _dir_arg(rest), options );
_print_build( result, options );
return result{ok} ? 0 : 1;
}
if ( command eq "upload" ) {
let result := box.upload( _upload_archive_arg(rest), options );
_print_upload( result, options );
return result{ok} ? 0 : ( result.get( "auth_error", false ) ? 3 : 1 );
}
}
catch ( Exception e ) {
if ( _has( options, "json" ) ) {
STDOUT.print(_plain_json({
ok: false,
error: e{message},
}));
}
else {
STDERR.say("zuzubox: " _ e{message});
}
return 1;
}
STDERR.say("Unknown command: " _ command);
STDERR.print(_usage());
return 2;
}
modules/zdf/zuzubox.zzm
zuzubox-0.0.1 source code
Package
- Name
- zuzubox
- Version
- 0.0.1
- Uploaded
- 2026-06-16 22:33:57
- Repository
- https://github.com/tobyink/zuzu-zuzubox
- Dependencies
-
-
std/archive>= 0 -
std/data/json>= 0 -
std/getopt>= 0 -
std/io>= 0 -
std/net/http>= 0 -
std/proc>= 0 -
std/string>= 0 -
std/time>= 0 -
std/tui>= 0 -
test/parser>= 0
-
- Metadata
- zuzu-distribution.json
- Archive
- Download .tar.gz