build / build (push) Successful in 21m27s
The packaged copies use config.default_path() and pipeline._history_path(): files next to the sources in a checkout, ~/.config/<name> and ~/.local/state/<name> when installed. The copies now differ from the originals only in the neutral defaults. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
174 lines
6.7 KiB
Python
174 lines
6.7 KiB
Python
"""Command-line mode: the same pipeline as the TUI, for scripts and quick publishing."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from . import __version__, config, detect, packages, pipeline, remote
|
|
from .pipeline import Options, Plan
|
|
|
|
SIGN = {"new": ("+", "32"), "replace": ("~", "33"), "meta": ("~", "36"), "notes": ("~", "36"),
|
|
"delete": ("-", "31"), "same": ("=", "2"), "skip": ("!", "33")}
|
|
COLOR = True
|
|
|
|
|
|
def paint(text: str, code: str) -> str:
|
|
return f"\033[{code}m{text}\033[0m" if COLOR else text
|
|
|
|
|
|
def print_plan(plan: Plan) -> None:
|
|
a = plan.app
|
|
print(f"\n{a.name} {a.src} -> {plan.server.target}:{plan.server.apps_dir}"
|
|
f" {('rev ' + plan.git_rev) if plan.git_rev else ''}")
|
|
for i in plan.items:
|
|
sign, code = SIGN[i.action]
|
|
print(paint(f" {sign} {i.describe(a)}", code))
|
|
for n in plan.notes:
|
|
print(f" ! {n}")
|
|
|
|
|
|
def confirm(question: str, yes: bool) -> bool:
|
|
if yes:
|
|
return True
|
|
if not sys.stdin.isatty():
|
|
print("not a terminal: pass --yes to confirm", file=sys.stderr)
|
|
return False
|
|
return input(f"{question} [y/N] ").strip().lower() in ("y", "yes", "т", "так")
|
|
|
|
|
|
def notes_text(args) -> str:
|
|
if args.notes_file:
|
|
return Path(args.notes_file).expanduser().read_text()
|
|
return (args.notes or "").replace("\\n", "\n")
|
|
|
|
|
|
def options(args) -> Options:
|
|
return Options(build=not args.no_build, force=args.force, prune=not args.no_prune, meta=not args.no_meta,
|
|
notes=notes_text(args))
|
|
|
|
|
|
def cmd_list(cfg, args):
|
|
st, others = pipeline.status_all(cfg)
|
|
style = {"newer": "1;32", "unpublished": "33", "older": "33", "error": "31", "no build": "2"}
|
|
for a in cfg.apps:
|
|
s = st[a.name]
|
|
print(f"{a.name:22} " + paint(f"{s.state:12}", style.get(s.state, "0"))
|
|
+ f" local: {s.local or '-':32} site: {s.remote or '-'}")
|
|
if others:
|
|
print(f"\nnot in {cfg.path.name}: {', '.join(others)}")
|
|
|
|
|
|
def cmd_publish(cfg, args):
|
|
names = [a.name for a in cfg.apps] if args.all else args.apps
|
|
if not names:
|
|
print("name apps or pass --all", file=sys.stderr)
|
|
return 1
|
|
if args.all:
|
|
st, _ = pipeline.status_all(cfg)
|
|
names = [n for n in names if st[n].state in ("newer", "unpublished")]
|
|
print(f"newer locally: {', '.join(names) or 'none'}")
|
|
rc = 0
|
|
for name in names:
|
|
plan = pipeline.preview(cfg, cfg.app(name), options(args), log=print)
|
|
print_plan(plan)
|
|
if args.dry_run or not plan.changes:
|
|
continue
|
|
if not confirm(f"publish {name}?", args.yes):
|
|
rc = rc or 1
|
|
continue
|
|
res = pipeline.execute(plan, log=print)
|
|
rc = rc if res.ok else 2
|
|
return rc
|
|
|
|
|
|
def cmd_versions(cfg, args):
|
|
for p in pipeline.versions(cfg, cfg.app(args.app)):
|
|
print(f"{p.version:14} {p.kind.label:9} {p.size / 1048576:8.1f} MB {p.file}")
|
|
return 0
|
|
|
|
|
|
def cmd_remove(cfg, args):
|
|
app = cfg.app(args.app)
|
|
if not confirm(f"delete {app.name} v{args.version} from the site?", args.yes):
|
|
return 1
|
|
res = pipeline.remove(cfg, app, args.version, log=print)
|
|
print(res.message)
|
|
return 0 if res.ok else 2
|
|
|
|
|
|
def cmd_add(cfg, args):
|
|
app = detect.new_app(Path(args.path), args.name)
|
|
print(config.app_to_toml(app))
|
|
try:
|
|
builds, notes = packages.resolve(app)
|
|
for b in builds:
|
|
print(f"# found {b.path.name} -> {b.site_name(app.name)}")
|
|
for n in notes:
|
|
print(f"# {n}")
|
|
except packages.PackageError as e:
|
|
print(f"# {e}")
|
|
if confirm(f"append to {cfg.path}?", args.yes):
|
|
config.append_apps(cfg.path, [app])
|
|
return 0
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
p = argparse.ArgumentParser(prog="publish", description="Publish apk, AppImage and binaries to the apps page "
|
|
"of the home Tomcat. No command: start the TUI.")
|
|
p.add_argument("--config", default=None, help="publish.toml (default: next to publish.py)")
|
|
p.add_argument("--no-color", action="store_true")
|
|
p.add_argument("-V", "--version", action="version", version=__version__)
|
|
sub = p.add_subparsers(dest="cmd")
|
|
|
|
sub.add_parser("list", help="local and published versions of the apps")
|
|
for name, hlp in (("publish", "preview and publish apps"), ("preview", "show what a publish would change")):
|
|
s = sub.add_parser(name, help=hlp)
|
|
s.add_argument("apps", nargs="*")
|
|
s.add_argument("-a", "--all", action="store_true", help="every app with a newer local build")
|
|
s.add_argument("-y", "--yes", action="store_true", help="do not ask for confirmation")
|
|
s.add_argument("-n", "--dry-run", action="store_true", default=name == "preview")
|
|
s.add_argument("-m", "--notes", help="what's new in this version (\\n for new lines)")
|
|
s.add_argument("--notes-file", help="what's new, from a file")
|
|
s.add_argument("--force", action="store_true", help="publish an older build, overwrite changed files")
|
|
s.add_argument("--no-build", action="store_true")
|
|
s.add_argument("--no-prune", action="store_true", help="keep versions beyond `keep`")
|
|
s.add_argument("--no-meta", action="store_true", help="do not upload the description and the icon")
|
|
s = sub.add_parser("versions", help="published packages of an app")
|
|
s.add_argument("app")
|
|
s = sub.add_parser("remove", help="unpublish a version (all its packages and notes)")
|
|
s.add_argument("app")
|
|
s.add_argument("version")
|
|
s.add_argument("-y", "--yes", action="store_true")
|
|
s = sub.add_parser("add", help="add a local project to publish.toml")
|
|
s.add_argument("path")
|
|
s.add_argument("--name")
|
|
s.add_argument("-y", "--yes", action="store_true")
|
|
sub.add_parser("check", help="validate publish.toml")
|
|
sub.add_parser("tui", help="start the TUI (default)")
|
|
|
|
args = p.parse_args(argv)
|
|
global COLOR
|
|
COLOR = sys.stdout.isatty() and not args.no_color
|
|
cfg_path = Path(args.config) if args.config else config.default_path()
|
|
try:
|
|
cfg = config.load(cfg_path)
|
|
except (OSError, config.ConfigError) as e:
|
|
print(f"config: {e}", file=sys.stderr)
|
|
return 1
|
|
if args.cmd in (None, "tui"):
|
|
from .tui import run
|
|
return run(cfg)
|
|
if args.cmd == "check":
|
|
print(f"{cfg_path}: {len(cfg.apps)} apps, OK")
|
|
return 0
|
|
handlers = {"list": cmd_list, "publish": cmd_publish, "preview": cmd_publish, "versions": cmd_versions,
|
|
"remove": cmd_remove, "add": cmd_add}
|
|
try:
|
|
return handlers[args.cmd](cfg, args) or 0
|
|
except (config.ConfigError, pipeline.PublishError, remote.RemoteError, OSError) as e:
|
|
print(f"error: {e}", file=sys.stderr)
|
|
return 1
|
|
except KeyboardInterrupt:
|
|
return 130
|