meta_package_manager.cli module

The mpm command-line interface: the group and its shared plumbing.

Defines the Click command group (global options, manager selection, the GlobalOptions state every subcommand reads) and the helpers several subcommand modules share: the inventory snapshot, the per-package action engine, the failure gates and the file-output guards.

The subcommands themselves live in one module per help section — meta_package_manager.cli_explore (the read-only queries), meta_package_manager.cli_maintenance (the state changers and diagnostics), meta_package_manager.cli_snapshots (manifest export and replay) and meta_package_manager.cli_sbom — imported at the bottom of this module so their @mpm.command registrations run. Each subcommand selects the managers from meta_package_manager.pool that implement the matching meta_package_manager.capabilities.Operations action, runs it across all of them, and renders the aggregated, multi-manager result.

Important

mpm keeps two output channels apart. The state of an operation prints with echo: the ✓/✘ trail and its finisher, gated by --progress (see meta_package_manager.dispatch). Log messages go through logging, gated by --verbosity, and each call belongs to one tier:

  • WARNING, the default view, holds problems only: a failure with no other signal on screen, the capped diagnosis of a failed command, a safety notice (cooldown safeguard skipped, file about to be overwritten, silent call that may hide a sudo prompt), the end-of-run error summary and timeouts. critical is for fatal conditions. Keep this tier sparse.

  • INFO holds the narration: the selection summary, the install priority, per-manager announcements, discovery, capability skips, options with no effect, and each CLI invocation run on the system.

  • DEBUG holds the mechanics: raw CLI output, version probes, refiltering, selection parsing and data dumps.

A line that narrates a decision, a step or a command is INFO. An option that had no effect is INFO too, never WARNING. A problem that the trail already shows needs no WARNING.

A manager-scoped line names its manager through a label, never in its message: extra={"label": manager.subject} inside an operation, manager.id outside one. The formatter of click-extra glues the label into the level prefix (warning:gem.installed:), so a log greps by manager. The ID stays in the prose where it is the object of the sentence, or names a configuration artifact.

An enum shown in a message renders as its bare member name, so it defines __str__ and __format__ to return self.name, as Operations does.

Important

Three exit contracts. The action commands (install, remove, upgrade <packages>, restore) collect per-package failures and exit 1 on a critical: summary, through exit_on_failures(). The maintenance commands (sync, cleanup, upgrade --all) are best-effort: a failed manager gets a ✘ and the exit stays 0. doctor reads health from the exit code of each diagnostic command, relays the native report verbatim to <stdout>, and exits 1 when a manager reports problems. -0/--zero-exit opts out of both gates. Usage and configuration errors exit 2 in every case.

meta_package_manager.cli.SECTIONS = (Section(Explore subcommands, is_sorted=False), Section(Maintenance subcommands, is_sorted=False), Section(Package snapshots subcommands, is_sorted=False), Section(SBOM subcommands, is_sorted=False))

Help-screen order of the subcommand sections, from the broadest to the narrowest.

Cloup otherwise ranks a section by the first subcommand registered into it, which here is whichever module the import block at the bottom of this file reaches first. That block is sorted by module name, so the order was an accident of the alphabet.

meta_package_manager.cli.UNMAINTAINED_MARKER = '(unmaintained)'

Marker closing the help of a selector whose manager is unmaintained.

Click writes (DEPRECATED), or (DEPRECATED: {reason}) when the flag carries one. Neither is what these selectors mean: the option is not being retired, the tool behind it was abandoned while mpm keeps driving it, which is why the manager index marks it ⚠️ rather than dropping it. One word carries that, and unlike Click’s it fits on the line it annotates.

The wording is uniform across the unmaintained managers rather than drawn from each unmaintained_message: those are markdown paragraphs with links, sized for the manager’s own page, where a help label has one line to spend.

Note

click-extra paints Click’s own spelling and no other (DEPRECATED_RE matches the literal word), so this marker renders unstyled where Click’s took the deprecated theme slot.

Todo

Declare the marker so it takes that slot back, once a click-extra release carries a deprecated field on HelpKeywords:

mpm.extra_keywords = HelpKeywords(…, deprecated={UNMAINTAINED_MARKER})

Naming it on a release without the field raises TypeError at import, so this waits on the floor rather than being guarded.

meta_package_manager.cli.UNMAINTAINED_REASON = 'upstream is unmaintained'

Why UNMAINTAINED_MARKER’s manager is flagged, for Click’s own notice.

Read only at runtime, appended to the DeprecationWarning: The option ‘volta’ is deprecated. line Click prints on stderr when someone actually selects one. The help screen never shows it: ManagerSelector keeps Click from labelling the option and writes UNMAINTAINED_MARKER instead.

meta_package_manager.cli.XKCD_MANAGER_ORDER = ('pip', 'brew', 'npm', 'dnf', 'apt', 'steamcmd')

Sequence of package managers as defined by XKCD #1654: Universal Install Script.

See the corresponding implementation rationale in issue #10.

class meta_package_manager.cli.GlobalOptions(all_managers, user_selection, user_drops, selected_managers, description, summary, network, progress, timeout)[source]

Bases: object

Global options and selection state every subcommand reads from ctx.obj.

Built once by the mpm group body, after the eager option callbacks have accumulated the manager selectors into the transient ctx.obj dict this instance replaces (see update_manager_selection()).

all_managers: bool

Include unsupported and unmaintained managers in the selection.

user_selection: list[str] | None

Managers explicitly selected by the user, in priority order, or None.

user_drops: set[str] | None

Managers explicitly excluded by the user, or None.

selected_managers: Callable[[...], Iterator[PackageManager]]

Resolve the target managers, applying selection and manager-level options.

description: bool

Show package description in results.

summary: bool

Print the end-of-run summary on stderr.

network: bool

Allow network calls during the run.

progress: bool

Whether progress indicators may render, after mpm’s output-mode gating.

The same value the group folds into each manager’s progress flag, exposed for the subcommands resolving their own managers instead of going through selected_managers (managers, whose only wait is the detection round the pool runs for it). The widget still auto-gates on an interactive stderr.

timeout: int | None

User-set maximum duration in seconds for each CLI call, or None.

meta_package_manager.cli.COOLDOWN_SUPPORTED_MANAGERS = ('flatpak', 'mas', 'npm', 'paru', 'pip', 'pipx', 'pnpm', 'uv', 'uvx', 'yay')

IDs of the managers that can enforce a release-age mpm --cooldown.

Derived from the pool so the --cooldown help text never drifts from the set of managers that actually enforce the window, whether through a native cooldown_env_var or through the per-package release_date probe: adding cooldown support to a manager surfaces it here automatically.

meta_package_manager.cli.RELEASE_INTRODUCING_OPERATIONS = frozenset({'install', 'upgrade'})

Operation names that bring new package versions onto the system.

The per-package cooldown hold of cooldown_hold only applies to these: remove introduces nothing, and read-only queries are never blocked. restore rides the install operation name, so it is covered.

meta_package_manager.cli.guard_existing_output(ctx, output_path, *, overwrite)[source]

Block clobbering an existing output file unless overwrite is set.

Warns and exits with code 2 when output_path already exists and the user did not pass --overwrite/--force/--replace. No-op when the file is absent. Callers handle the stdout case separately.

Return type:

None

meta_package_manager.cli.update_manager_selection(ctx, param, value)[source]

Update global selection list of managers in the context.

Accumulate and merge all manager selectors to form the initial population enforced by the user.

Return type:

None

class meta_package_manager.cli.ManagerSelector(*args, unmaintained=False, **kwargs)[source]

Bases: Option

The --<id>/--no-<id> boolean flag pair of one manager.

Collects every occurrence instead of keeping the last one, as a plain boolean flag would: exclusion takes precedence over inclusion whatever the order the two halves are given in, so --no-brew --brew must still show update_manager_selection() its --no-brew half.

Note

A configuration file reaches the option as a lone value through default_map, which Click cannot cast against a parameter collecting several. Wrapping it here is what lets brew = false land as one occurrence.

Mark the flag pair of an unmaintained manager, in mpm’s own words.

deprecated is set after Click has built the option rather than passed into it: Click appends its (DEPRECATED: …) label to the help string from the constructor, and reads the attribute again only at parse time. Assigning it afterwards therefore keeps the stderr notice a user gets for actually selecting one, while leaving the help line to UNMAINTAINED_MARKER.

type_cast_value(ctx, value)[source]

Read a lone configuration value as a single occurrence of the flag.

Return type:

Any

meta_package_manager.cli.single_manager_selectors()[source]

Dynamically creates a dedicated flag selector pair for each manager.

meta_package_manager.cli.bar_plugin_path(ctx, param, value)[source]

Print the location of the SwiftBar/Xbar plugin.

Returns the normalized path of the standalone bar_plugin.py script that is distributed with this Python module. This is made available under the mpm --bar-plugin-path option.

Notice that the fully-qualified home directory get replaced by its shorthand (~) if applicable:

  • the full /home/user/.python/site-packages/mpm/bar_plugin.py path is simplified to ~/.python/site-packages/mpm/bar_plugin.py,

  • but /usr/bin/python3.10/mpm/bar_plugin.py is returned as-is.

meta_package_manager.cli.install_teardown_interrupt_guard(ctx)[source]

Make a Ctrl+C pressed during teardown exit at once, quietly.

install_interrupt_handler() covers the run itself: the first Ctrl+C terminates every live subprocess, then aborts. It cannot cover what follows, because its own handler is restored when the context closes: the interpreter is still joining any worker thread whose child is draining, and a Ctrl+C landing inside that join surfaces as Exception ignored on threading shutdown, a dozen lines of threading internals ending on a bare KeyboardInterrupt. It reads as a crash, where the user only asked twice for the run to stop.

So this guard is armed by a close callback rather than during the run, and is deliberately never restored: nothing follows it but the teardown it covers. The exit skips the join instead of unwinding through it, with the shell’s conventional 128 + SIGINT. Everything the run had to say is already flushed by then, the abort having printed it.

Registered before install_interrupt_handler(), so that its restore runs first and this one has the last word: a context unwinds its close callbacks last-in first-out.

Skipped off a terminal, where nobody is pressing Ctrl+C and a handler nothing restores would outlive the call: the test suite drives this CLI in-process, over and over, in a process whose own Ctrl+C belongs to pytest.

Return type:

None

class meta_package_manager.cli.SourcedParamsOption(param_decls=None, *, is_flag=True, expose_value=False, is_eager=True, help='Show all CLI parameters, their provenance, defaults and value, then exit.', **kwargs)[source]

Bases: ShowParamsOption

--params with the config_file column drawn without being asked for.

click-extra keeps that column opt-in, reachable through a --columns projection on the root command. mpm has no such option: its --columns are per-subcommand, each with its own vocabulary of package fields, and --params is a root option that prints and exits before any of them is parsed. The column would therefore be unreachable, which is why it is drawn by default here instead.

It earns the width: mpm layers a user-wide configuration under a project’s own (see group_params()), so Source saying CONFIGURATION leaves the reader asking which file, and this column is the answer.

classmethod default_columns()[source]

Every non-opt-in column, plus config_file.

Return type:

tuple[ColumnSpec, ...]

meta_package_manager.cli.group_params()[source]

click-extra’s default parameters, with three of them swapped for ours.

--version is swapped by version_screen_params(), which this builds on. The other two are --config, which gains the cascade described below, and --params, which gains the column that makes the cascade readable.

Swapping instances in place keeps click-extra’s carefully ordered parameter sequence, and keeps the @group(config_schema=…, config_validators=…) arguments working: those are grafted onto whichever parameter is a ConfigOption, after the list is built.

Note

cascade=True layers every discovered configuration file, the most local one winning key by key. Without it exactly one file applies, so a machine-wide verbosity or manager exclusion was dropped in full the moment the working directory held a pyproject.toml carrying any [tool.mpm] key at all. An explicit --config never cascades: it pins one source, as it always did.

Return type:

list[Parameter]

meta_package_manager.cli.serialized_errors(manager)[source]

Serialize the distinct CLI errors manager accumulated so far.

The errors entry of every serialized payload, in the shape the ✓/✘ trail and the bar plugin read it.

Collected at the last minute — after the manager’s query ran — so the list gathers everything the run produced. A non-empty list marks the manager’s ✘ in the concurrent spinner trail (see meta_package_manager.dispatch.collect_from_managers()) and ships in the serialized payloads.

Return type:

list[str]

meta_package_manager.cli.failure_detail(errors)[source]

The cause a failed attempt shows on its ✘ trail line, if mpm knows it.

The first detail among the errors of the attempt, which new_errors() collects. A plain failure carries none: the glyph already says the attempt failed.

Return type:

str | None

meta_package_manager.cli.snapshot_installed(manager, query, *, exact)[source]

Materialize the manager’s installed inventory, filtered by query.

The shared fetch of every inventory consumer (installed, dump, dump --brewfile, sbom): a best-effort installed_or_empty() snapshot (a broken manager yields no packages instead of aborting the batch), post-filtered through filter_matches().

Return type:

tuple[Package, ...]

meta_package_manager.cli.filter_matches(packages, query, *, exact)[source]

Yield only the packages matching query on their ID or name.

A transparent pass-through when query is None (no positional query was given). Shared by the installed and outdated subcommands to post-filter the fully-materialized package list each manager returns: unlike search, these operations already hold the complete inventory, so the query is a local refinement rather than a manager-side lookup. Mirrors the fuzzy/--exact semantics of search through meta_package_manager.package.Package.matches().

Return type:

Iterator[Package]

meta_package_manager.cli.query_option(f)

--query filter of the inventory exporters (dump, sbom).

Return type:

TypeVar(F, bound= Callable[..., Any])

meta_package_manager.cli.query_exact_option(f)

--exact refinement of query_option.

Return type:

TypeVar(F, bound= Callable[..., Any])

meta_package_manager.cli.overwrite_option(f)

Opt-in clobbering of an existing output file (dump, sbom); see guard_existing_output().

Return type:

TypeVar(F, bound= Callable[..., Any])

meta_package_manager.cli.package_label(spec)[source]

Render a spec as package_id or package_id@version for trail output.

Return type:

str

meta_package_manager.cli.run_manager_action(manager, spec, *, action, verb, operation)[source]

Run action(manager, spec) for one package, returning success.

The shared core of every per-package attempt (install, remove, upgrade <packages>, restore, and both install dispatch paths). The action runs under acting_as() with operation stamped (resolving the right per-operation timeout and watchdog policy, scoped to the attempt) and the manager forced to raise on failure, so a botched operation is recorded by the caller rather than silently swallowed. Narrates the per-attempt reason at INFO and logs the CLI output on success. The caller maps the boolean onto its own ✓/✘ ledger and retry/stop semantics.

Return type:

bool

meta_package_manager.cli.install_action(manager, spec)[source]

The canonical install action, shared by install and restore.

After the install, a package that the manager may already hold as a dependency is marked as explicitly installed, so an orphan sweep keeps it (see mark_explicit()). The check runs before the install and under the stamp of the read it does, so mpm --plan runs it and captures only the mutations.

Return type:

str | None

meta_package_manager.cli.package_task(manager, spec, lock, *, action, verb, operation, record_failure)[source]

Build one per-package task for collect_per_package().

Runs the attempt through run_manager_action() and returns (ok, message) for the ✓/✘ trail. On failure it appends the spec to a caller-owned list through record_failure (under lock, since the list is shared across the concurrent lanes) and reports ✘. Shared by install, remove, upgrade <packages> and restore, whose tasks differ only in the action, the verb, and which failure list they feed. Each outcome reads brew.install: jq on the trail (see trail_label()).

Parameters:
  • action (Callable[[PackageManager, Specifier], str | None]) – performs the manager operation, returning its CLI output (or None).

  • verb (str) – present-tense operation name (“install”), for the INFO lines.

  • operation (str) – Operations member name stamped on the manager for the duration of the attempt.

  • record_failure (Callable[[Specifier], None]) – appends the failed spec’s label to a caller-owned list.

Return type:

Callable[[], tuple[bool, str]]

meta_package_manager.cli.fail_unless_zero_exit(ctx, message)[source]

Print the durable critical: :message:` record, then exit `1 unless -0/--zero-exit opted out of the gate.

The shared failure gate of the action commands (exit_on_failures()) and doctor: the summary always prints, following the linter convention where findings gate automation, and -0 keeps the exit code at 0 with the printed summary staying the durable record. Usage and configuration errors are unaffected: they exit 2 regardless, as genuine execution failures.

Return type:

None

meta_package_manager.cli.exit_on_failures(ctx, verb, failures)[source]

Report the per-package failures collected this run and exit non-zero.

A no-op when failures is empty. Otherwise routes the deduplicated, sorted Could not {verb}: ... summary through fail_unless_zero_exit(). Shared by every action command (install, remove, upgrade <packages>, restore).

Return type:

None

meta_package_manager.cli.package_outcomes(before, after, expected, hold)[source]

Classify every package a command moved, or should have.

before and after are the two installed_inventory() readings framing a manager’s part of the command. expected is the outdated_inventory() an upgrade takes before it runs, and None for a command that expects no package to move in particular. hold is the manager’s cooldown_hold(). Each row names the package and its versions and carries one PackageOutcome as status:

  • a version that differs between the two readings is downgraded when it moved back to an older release, and upgraded otherwise, a move whose direction the version ordering cannot tell included;

  • a package only the second reading lists is installed: one the command installed, or a dependency it pulled in. One only the first lists is removed;

  • an expected package whose version did not move is cooldown when the cooldown still holds it back, still outdated otherwise: a failed build, a pinned package, or one the native command leaves alone. Its to_version is the one still available. A cooldown row also dates that version when the hold could: released is its publication, eligible the instant it clears the cooldown window.

An expected package neither reading lists cannot be told moved from unmoved, so it gets no row rather than a guess. Rows come sorted by package ID.

Return type:

list[dict[str, object]]

meta_package_manager.cli.outcome_detail(rows, results)[source]

Count the results among a manager’s rows, for its trail line.

Reads 1 upgraded, 2 cooldown. Always opens on the first of results, zero included (nothing upgraded), so a run that changed nothing says so where a run that changed something says how much. The other results follow when there are any. An outcome left out of results stays in the table, being a consequence rather than a result: the dependencies an upgrade pulled in or dropped.

Return type:

str

meta_package_manager.cli.print_change_report(ctx, report)[source]

Print what a command changed in the installed inventory.

report is keyed by manager ID, each payload shaped like the installed and outdated ones (id, name, packages, errors) with the rows of package_outcomes() as packages. A serialization --table-format gets the whole payload, and the command goes on to its own exit code. Otherwise the rows render as one table through CHANGE_REPORT_COLUMNS, the version pair of a package colored the way mpm outdated colors it, and its status shown as its label. An empty table prints nothing: the trail already said nothing moved.

The two COOLDOWN_COLUMNS show each timestamp as its date in UTC. They join the default selection when a row carries one, so a report where the cooldown dated nothing does not grow two empty columns.

Return type:

None

class meta_package_manager.cli.ChangeReport(managers=())[source]

Bases: object

What a command did to the installed inventory of each manager.

Frames each manager’s part of the command with two installed_inventory() readings, open() before and close() after, and classifies their difference through package_outcomes(). A native command reports what it changed in its own words, or not at all: the diff gives every manager the same report, whatever it printed. Shared by install, remove, upgrade, restore and cleanup --orphans, each closing on show().

No reading is taken under --dry-run, whose simulated reads list nothing, nor under --plan, whose captured mutations move nothing: the diff would then read as a run that changed nothing.

Thread-safe: a concurrent fan-out opens and closes different managers on different workers, and a manager’s own tasks never overlap.

managers fixes the order of the report up front, for a fan-out whose managers finish in any order. A manager opened later joins at the end.

open(manager, *, outdated=False, among=None)[source]

Take the reading before manager changes its inventory.

With outdated, also read the packages the command is expected to move, kept to the IDs in among when given, and return them: upgrade --all hands them to the upgrade, so it does not list them twice. Returns None otherwise, or when the manager cannot be read.

A manager already open keeps its first reading, so a command attempting one manager several times frames all of its attempts.

Return type:

dict[str, Package] | None

close(manager)[source]

Take the reading after manager changed its inventory, and file the difference as the manager’s rows of the report.

Returns the rows, or None when the manager stays out of the report: it was never open, its second reading failed, or a reading carries no version at all.

Return type:

list[dict[str, object]] | None

close_all()[source]

Close every manager still open: the sequential install path opens each manager on its first attempt and closes them all at the end.

Return type:

None

bracket(tasks, *, outdated=None)[source]

Wrap per-package tasks so each manager’s run is read on both sides.

The tasks come back grouped by manager, each manager’s in their original order: its first task opens it and its last closes it. Grouping keeps the members of a lock family from interleaving on their shared lane, so each pair of readings frames the tasks of its own manager alone. The first reading is taken there, right before the first task, and never reused from an earlier listing like the one sourcing a spec: a peer’s reading must see what the peer ahead of it on the lane changed. outdated maps a manager ID to the package IDs its tasks are expected to move (see open()).

Return type:

list[tuple[PackageManager, Callable[[], tuple[bool, str]]]]

property payload: dict[str, dict]

The report so far, one payload per manager read on both sides.

show(ctx)[source]

Print the report through print_change_report(), when any manager could be read on both sides of the command.

Return type:

None