monochange 0.1.0

Manage versions and releases for your multiplatform, multilanguage monorepo
Documentation
use std::fmt::Write as _;

use monochange_core::MonochangeError;
use monochange_core::MonochangeResult;
use serde_json::json;

use crate::AssistOutputFormat;
use crate::Assistant;

pub(crate) fn assistant_display_name(assistant: Assistant) -> &'static str {
	match assistant {
		Assistant::Generic => "Generic MCP client",
		Assistant::Claude => "Claude",
		Assistant::Cursor => "Cursor",
		Assistant::Copilot => "GitHub Copilot",
		Assistant::Pi => "Pi",
	}
}

pub(crate) fn assistant_setup_payload(assistant: Assistant) -> serde_json::Value {
	let mcp_config = json!({
		"mcpServers": {
			"monochange": {
				"command": "monochange",
				"args": ["mcp"]
			}
		}
	});
	let guidance = vec![
		"Read `monochange.toml` before proposing release workflow changes.".to_string(),
		"Run `mc validate` before and after release-affecting edits.".to_string(),
		"Use `mc discover --format json` to inspect package ids, group ownership, and dependency edges.".to_string(),
		"Prefer `mc change` plus `.changeset/*.md` files over ad hoc release notes when encoding release intent.".to_string(),
		"Use `mc release --dry-run --format json` before any mutating release command or source-provider publish flow.".to_string(),
	];
	let notes = match assistant {
		Assistant::Generic => vec![
			"Add the MCP snippet to any client that supports stdio MCP servers.".to_string(),
			"Install `@monochange/skill` when you want a reusable local skill bundle with the same repo guidance.".to_string(),
		],
		Assistant::Claude => vec![
			"Add the MCP snippet to Claude's MCP configuration and keep the repo-local guidance in project instructions.".to_string(),
			"Use the skill package as a reviewable source of guidance rather than embedding one-off release instructions in each chat.".to_string(),
		],
		Assistant::Cursor => vec![
			"Configure the MCP server in Cursor at the workspace or user level.".to_string(),
			"Pair MCP with repo instructions so Cursor agents validate and dry-run release changes before editing manifests or changelogs.".to_string(),
		],
		Assistant::Copilot => vec![
			"Use this MCP snippet in Copilot or VS Code environments that support MCP-compatible server definitions.".to_string(),
			"Keep the guidance close to workspace instructions so Copilot follows monochange's explicit changeset and dry-run workflow.".to_string(),
		],
		Assistant::Pi => vec![
			"Install `@monochange/skill` and copy the bundled files into your Pi skills directory when you want reusable monochange-specific instructions.".to_string(),
			"Configure Pi to run `monochange mcp` so agents can inspect the workspace model, create changesets, and preview releases through MCP tools.".to_string(),
		],
	};

	json!({
		"assistant": assistant_display_name(assistant),
		"strategy": {
			"type": "official-profile",
			"scope": "config-snippets-guidance-install",
			"summary": "monochange ships official assistant setup guidance with install steps, MCP config, and repo-local workflow rules."
		},
		"install": {
			"cli": {
				"npm": "npm install -g @monochange/cli",
				"cargo": "cargo install monochange"
			},
			"skill": {
				"npm": "npm install -g @monochange/skill",
				"copy": "monochange-skill --copy ~/.pi/agent/skills/monochange"
			}
		},
		"mcp_config": mcp_config,
		"repo_guidance": guidance,
		"notes": notes,
	})
}

pub(crate) fn run_assist(
	assistant: Assistant,
	format: AssistOutputFormat,
) -> MonochangeResult<String> {
	let payload = assistant_setup_payload(assistant);

	match format {
		AssistOutputFormat::Json => {
			serde_json::to_string_pretty(&payload)
				.map_err(|error| MonochangeError::Config(error.to_string()))
		}

		AssistOutputFormat::Text => {
			let mcp_config = serde_json::to_string_pretty(&payload["mcp_config"])
				.map_err(|error| MonochangeError::Config(error.to_string()))?;
			let install = serde_json::to_string_pretty(&payload["install"])
				.map_err(|error| MonochangeError::Config(error.to_string()))?;

			let mut output = String::new();
			let _ = writeln!(output, "monochange assist");
			let _ = writeln!(output);
			let _ = writeln!(
				output,
				"Assistant                 {}",
				payload["assistant"].as_str().unwrap_or_default()
			);
			let _ = writeln!(
				output,
				"Strategy                  {}",
				payload["strategy"]["summary"].as_str().unwrap_or_default()
			);
			let _ = writeln!(output);
			let _ = writeln!(output, "Install:");
			let _ = writeln!(output, "{install}");
			let _ = writeln!(output);
			let _ = writeln!(output, "MCP config snippet:");
			let _ = writeln!(output, "{mcp_config}");
			let _ = writeln!(output);
			let _ = writeln!(output, "Suggested repo-local guidance:");

			for item in payload["repo_guidance"].as_array().into_iter().flatten() {
				if let Some(text) = item.as_str() {
					let _ = writeln!(output, "- {text}");
				}
			}

			let _ = writeln!(output);
			let _ = writeln!(output, "Notes for {}:", assistant_display_name(assistant));

			for item in payload["notes"].as_array().into_iter().flatten() {
				if let Some(text) = item.as_str() {
					let _ = writeln!(output, "- {text}");
				}
			}

			Ok(output.trim_end().to_string())
		}
	}
}