Files
Das-OS/srcpkgs/publisher/files/python/publisher/cli.py
T
dasandClaude Sonnet 5.5 60af93df74
build / build (push) Successful in 21m27s
publisher, deployer: same path logic as the originals
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>
2026-10-25 14:55:40 +02:00

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