Skip to content

Plugin system - context ingestion #57

@codcod

Description

@codcod

Goal

Centralize core options (config loading, tag/exclude filtering) so plugins receive ready data and only implement their own functionality.

Minimal Design

1. Core Adds PluginContext

Contains:

  • &Config (already loaded)
  • Vec (already filtered)
  • Raw args after plugin name (plugin-specific)
  • Debug flag (optional)
  • Mode (optional string for multi-mode plugins)

2. Unified Plugin Trait

pub trait Plugin {
    fn name(&self) -> &'static str;
    fn run(&self, ctx: PluginContext) -> anyhow::Result<()>;
}

3. Core Flow (repos CLI)

  1. Parse global options: --config, --tag, --exclude-tag, --debug
  2. Load config, filter repositories
  3. Detect plugin invocation: repos health [plugin-args...]
  4. Build PluginContext
  5. Dispatch to registered plugin.

4. Registration (static table)

static PLUGINS: &[&dyn Plugin] = &[&HealthPlugin];

5. Health Plugin Refactor (remove its own arg parsing)

Only interpret plugin-specific flags (e.g. mode: deps/prs, --debug optional). Fallback to default mode if absent. No config/tag parsing.

File Changes (Scaffold)

use crate::{Config, Repository};

pub struct PluginContext<'a> {
    pub config: &'a Config,
    pub repositories: &'a [Repository],
    pub args: Vec<String>,      // plugin-specific args after plugin name
    pub debug: bool,
}

impl<'a> PluginContext<'a> {
    pub fn new(config: &'a Config,
               repositories: &'a [Repository],
               args: Vec<String>,
               debug: bool) -> Self {
        Self { config, repositories, args, debug }
    }
}
pub mod context;

use anyhow::Result;
use context::PluginContext;

pub trait Plugin {
    fn name(&self) -> &'static str;
    fn run(&self, ctx: PluginContext) -> Result<()>;
}

// Simple lookup
pub fn find_plugin(name: &str) -> Option<&'static dyn Plugin> {
    match name {
        "health" => Some(&crate::plugins::health::HealthPlugin),
        _ => None,
    }
}
use anyhow::Result;
use crate::plugin::{Plugin, context::PluginContext};

pub struct HealthPlugin;

impl Plugin for HealthPlugin {
    fn name(&self) -> &'static str { "health" }

    fn run(&self, ctx: PluginContext) -> Result<()> {
        // Parse plugin-local args: mode (deps|prs), optional --debug override
        let mut mode = "deps";
        let mut debug = ctx.debug;
        let mut i = 0;
        while i < ctx.args.len() {
            match ctx.args[i].as_str() {
                "deps" | "prs" => { mode = &ctx.args[i]; i += 1; }
                "--debug" | "-d" => { debug = true; i += 1; }
                "--help" | "-h" => { print_health_help(); return Ok(()); }
                _ => { eprintln!("health: unknown arg {}", ctx.args[i]); i += 1; }
            }
        }
        match mode {
            "deps" => run_deps(ctx.repositories, debug),
            "prs" => run_prs(ctx.repositories, debug),
            _ => Ok(()),
        }
    }
}

// Existing logic in run_deps_check / run_pr_report would be adapted to run_deps / run_prs
fn print_health_help() { /* minimal help */ }
fn run_deps(repos: &[crate::Repository], debug: bool) -> Result<()> { /* existing body */ Ok(()) }
fn run_prs(repos: &[crate::Repository], debug: bool) -> Result<()> { /* existing body */ Ok(()) }
// ...existing code...
if let Some(first) = args.get(1) {
    if let Some(plugin) = crate::plugin::find_plugin(first) {
        // Collect plugin-specific args (after plugin name)
        let plugin_args = args.iter().skip(2).cloned().collect::<Vec<_>>();
        let ctx = PluginContext::new(&config, &filtered_repos, plugin_args, debug_flag);
        return plugin.run(ctx);
    }
}
// ...existing code...

Migration Steps

  1. Introduce PluginContext + Plugin trait.
  2. Wrap existing health plugin logic (move code, preserve functions).
  3. Change CLI entry to detect plugin name early.
  4. Remove config/tag parsing from plugin main; rely on core.

Benefits

  • Plugins focus purely on feature code.
  • Single source of truth for config + repository filtering.
  • Easier future plugins (add struct + register).
  • Reduced duplication and error surface.

Minimal Changes Principle

  • No change to existing filtering logic; only relocation.
  • Health functionality unchanged; argument surface same.
  • No broad refactors; added small, isolated modules.

Impact summary:

  • Internal plugins (implementing Plugin trait) will receive PluginContext directly.
  • External plugins discovered on PATH (repos-*) will continue working unchanged unless you opt into passing them preprocessed context.
  • No breaking change required if you keep current exec pathway as a fallback.

Recommended compatibility approach:

  1. Keep existing external plugin execution

Core still resolves an external plugin binary (e.g. repos-health) and passes remaining args. External plugins keep parsing --config, --tag, --exclude-tag if they want. No immediate disruption.

  1. Add context injection for external plugins (optional, non-breaking)

Before spawning the external plugin, core prepares filtered repositories and makes them available via environment variables or temp file:

// ...existing code...
fn exec_external_plugin(bin: &Path, plugin_args: &[String], ctx: &PluginContext) -> anyhow::Result<()> {
    // Serialize filtered repos minimally
    let tmp = tempfile::NamedTempFile::new()?;
    serde_json::to_writer(&tmp, &ctx.repositories)?;
    let repos_file = tmp.path().to_string_lossy().to_string();

    let status = std::process::Command::new(bin)
        .args(plugin_args)
        .env("REPOS_CONFIG_PATH", ctx.config.source_path().unwrap_or_default())
        .env("REPOS_FILTERED_REPOS_FILE", &repos_file)
        .env("REPOS_DEBUG", if ctx.debug { "1" } else { "0" })
        .env("REPOS_PLUGIN_PROTOCOL", "1")
        .status()?;

    if !status.success() {
        anyhow::bail!("external plugin exited with {}", status);
    }
    Ok(())
}
// ...existing code...

External plugin authors can then skip parsing global options and just read:

// External plugin example (standalone binary main)
fn main() -> anyhow::Result<()> {
    if let Ok(path) = std::env::var("REPOS_FILTERED_REPOS_FILE") {
        let data = std::fs::read(path)?;
        let repos: Vec<Repository> = serde_json::from_slice(&data)?;
        // Only implement plugin-specific behavior
    } else {
        // Fallback: legacy path, parse CLI flags manually
    }
    Ok(())
}
  1. Gradual migration
  • Phase 1: Introduce Plugin trait + internal PluginContext (no change to external plugins).
  • Phase 2: Add env injection when executing external plugins.
  • Phase 3 (optional): Deprecate global flag parsing for external plugins by documenting new env protocol; keep backwards compatibility indefinitely.
  1. Detection logic
  • If internal plugin matches name: run via trait.
  • Else if binary exists on PATH: create PluginContext, pass to exec_external_plugin.
  • Else: unknown command.
  1. No forced rewrites

External plugins that do not implement the Plugin trait:

  • Continue to function exactly as today.
  • Can opt-in to simplified model by checking REPOS_FILTERED_REPOS_FILE.
  • If they ignore new env vars, behavior is unchanged.
  1. Failure isolation

If context serialization fails, fall back to legacy invocation to avoid breaking existing plugins.

Minimal core changes (high level):

  • Add PluginContext struct.
  • Add internal plugin registry.
  • Wrap existing external plugin execution adding env injection (non-breaking).
  • Do not remove existing argument pass-through.

Benefits:

  • Internal plugins get zero boilerplate.
  • External plugins can progressively adopt context without mandatory rewrites.
  • Maintains absolute minimal change and backwards compatibility.

Next step (if approved):
Implement exec_external_plugin modification plus PluginContext addition; leave health plugin transition for a later isolated PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions