import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js";
import { o as createPluginLoaderLogger, s as loadSymiPlugins } from "./unified-runner-4O0ifBsI.js";
import { a as formatChannelSelectionLine, i as formatChannelPrimerLine, s as listChatChannels } from "./registry-Sbac4a4z.js";
import { t as createSubsystemLogger } from "./subsystem-CHbO_DkH.js";
import { t as formatCliCommand } from "./command-format-BzjsXBTi.js";
import { m as normalizeAccountId, p as DEFAULT_ACCOUNT_ID } from "./session-key-DUSb7CCb.js";
import { c as resolveAgentWorkspaceDir, l as resolveDefaultAgentId } from "./agent-scope-DOrZjOnW.js";
import { n as listChannelPlugins, t as getChannelPlugin } from "./plugins-CF5skkHh.js";
import { t as formatDocsLink } from "./links-Bqr0FxXx.js";
import { r as listChannelPluginCatalogEntries } from "./catalog-BptA-dUv.js";
import { t as resolveChannelDefaultAccountId } from "./helpers-G6zuddmF.js";
import { n as isChannelConfigured } from "./plugin-auto-enable-DJBhDIMq.js";
import { t as enablePluginInConfig } from "./enable-B8TbkmWU.js";
import { n as installPluginFromNpmSpec, t as recordPluginInstall } from "./installs-D890uIIW.js";
import path from "node:path";
import fs from "node:fs";

//#region src/commands/onboarding/plugin-install.ts
function hasGitWorkspace(workspaceDir) {
	const candidates = /* @__PURE__ */ new Set();
	candidates.add(path.join(process.cwd(), ".git"));
	if (workspaceDir && workspaceDir !== process.cwd()) candidates.add(path.join(workspaceDir, ".git"));
	for (const candidate of candidates) if (fs.existsSync(candidate)) return true;
	return false;
}
function resolveLocalPath(entry, workspaceDir, allowLocal) {
	if (!allowLocal) return null;
	const raw = entry.install.localPath?.trim();
	if (!raw) return null;
	const candidates = /* @__PURE__ */ new Set();
	candidates.add(path.resolve(process.cwd(), raw));
	if (workspaceDir && workspaceDir !== process.cwd()) candidates.add(path.resolve(workspaceDir, raw));
	for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
	return null;
}
function addPluginLoadPath(cfg, pluginPath) {
	const existing = cfg.plugins?.load?.paths ?? [];
	const merged = Array.from(new Set([...existing, pluginPath]));
	return {
		...cfg,
		plugins: {
			...cfg.plugins,
			load: {
				...cfg.plugins?.load,
				paths: merged
			}
		}
	};
}
async function promptInstallChoice(params) {
	const { entry, localPath, prompter, defaultChoice } = params;
	const localOptions = localPath ? [{
		value: "local",
		label: "Use local plugin path",
		hint: localPath
	}] : [];
	const options = [
		{
			value: "npm",
			label: `Download from npm (${entry.install.npmSpec})`
		},
		...localOptions,
		{
			value: "skip",
			label: "Skip for now"
		}
	];
	const initialValue = defaultChoice === "local" && !localPath ? "npm" : defaultChoice;
	return await prompter.select({
		message: `Install ${entry.meta.label} plugin?`,
		options,
		initialValue
	});
}
function resolveInstallDefaultChoice(params) {
	const { cfg, entry, localPath } = params;
	const updateChannel = cfg.update?.channel;
	if (updateChannel === "dev") return localPath ? "local" : "npm";
	if (updateChannel === "stable" || updateChannel === "beta") return "npm";
	const entryDefault = entry.install.defaultChoice;
	if (entryDefault === "local") return localPath ? "local" : "npm";
	if (entryDefault === "npm") return "npm";
	return localPath ? "local" : "npm";
}
async function ensureOnboardingPluginInstalled(params) {
	const { entry, prompter, runtime, workspaceDir } = params;
	let next = params.cfg;
	const localPath = resolveLocalPath(entry, workspaceDir, hasGitWorkspace(workspaceDir));
	const choice = await promptInstallChoice({
		entry,
		localPath,
		defaultChoice: resolveInstallDefaultChoice({
			cfg: next,
			entry,
			localPath
		}),
		prompter
	});
	if (choice === "skip") return {
		cfg: next,
		installed: false
	};
	if (choice === "local" && localPath) {
		next = addPluginLoadPath(next, localPath);
		next = enablePluginInConfig(next, entry.id).config;
		return {
			cfg: next,
			installed: true
		};
	}
	const result = await installPluginFromNpmSpec({
		spec: entry.install.npmSpec,
		logger: {
			info: (msg) => runtime.log?.(msg),
			warn: (msg) => runtime.log?.(msg)
		}
	});
	if (result.ok) {
		next = enablePluginInConfig(next, result.pluginId).config;
		next = recordPluginInstall(next, {
			pluginId: result.pluginId,
			source: "npm",
			spec: entry.install.npmSpec,
			installPath: result.targetDir,
			version: result.version,
			resolvedName: result.npmResolution?.name,
			resolvedVersion: result.npmResolution?.version,
			resolvedSpec: result.npmResolution?.resolvedSpec,
			integrity: result.npmResolution?.integrity,
			shasum: result.npmResolution?.shasum,
			resolvedAt: result.npmResolution?.resolvedAt
		});
		return {
			cfg: next,
			installed: true
		};
	}
	await prompter.note(`Failed to install ${entry.install.npmSpec}: ${result.error}`, "Plugin install");
	if (localPath) {
		if (await prompter.confirm({
			message: `Use local plugin path instead? (${localPath})`,
			initialValue: true
		})) {
			next = addPluginLoadPath(next, localPath);
			next = enablePluginInConfig(next, entry.id).config;
			return {
				cfg: next,
				installed: true
			};
		}
	}
	runtime.error?.(`Plugin install failed: ${result.error}`);
	return {
		cfg: next,
		installed: false
	};
}
function reloadOnboardingPluginRegistry(params) {
	const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg));
	const log = createSubsystemLogger("plugins");
	loadSymiPlugins({
		config: params.cfg,
		workspaceDir,
		cache: false,
		logger: createPluginLoaderLogger(log)
	});
}

//#endregion
//#region src/commands/onboarding/registry.ts
const CHANNEL_ONBOARDING_ADAPTERS = () => new Map(listChannelPlugins().map((plugin) => plugin.onboarding ? [plugin.id, plugin.onboarding] : null).filter((entry) => Boolean(entry)));
function getChannelOnboardingAdapter(channel) {
	return CHANNEL_ONBOARDING_ADAPTERS().get(channel);
}
function listChannelOnboardingAdapters() {
	return Array.from(CHANNEL_ONBOARDING_ADAPTERS().values());
}

//#endregion
//#region src/commands/onboard-channels.ts
var onboard_channels_exports = /* @__PURE__ */ __exportAll({
	noteChannelStatus: () => noteChannelStatus,
	setupChannels: () => setupChannels
});
function formatAccountLabel(accountId) {
	return accountId === DEFAULT_ACCOUNT_ID ? "default (primary)" : accountId;
}
async function promptConfiguredAction(params) {
	const { prompter, label, supportsDisable, supportsDelete } = params;
	const updateOption = {
		value: "update",
		label: "Modify settings"
	};
	const disableOption = {
		value: "disable",
		label: "Disable (keeps config)"
	};
	const deleteOption = {
		value: "delete",
		label: "Delete config"
	};
	const skipOption = {
		value: "skip",
		label: "Skip (leave as-is)"
	};
	const options = [
		updateOption,
		...supportsDisable ? [disableOption] : [],
		...supportsDelete ? [deleteOption] : [],
		skipOption
	];
	return await prompter.select({
		message: `${label} already configured. What do you want to do?`,
		options,
		initialValue: "update"
	});
}
async function promptRemovalAccountId(params) {
	const { cfg, prompter, label, channel } = params;
	const plugin = getChannelPlugin(channel);
	if (!plugin) return DEFAULT_ACCOUNT_ID;
	const accountIds = plugin.config.listAccountIds(cfg).filter(Boolean);
	const defaultAccountId = resolveChannelDefaultAccountId({
		plugin,
		cfg,
		accountIds
	});
	if (accountIds.length <= 1) return defaultAccountId;
	return normalizeAccountId(await prompter.select({
		message: `${label} account`,
		options: accountIds.map((accountId) => ({
			value: accountId,
			label: formatAccountLabel(accountId)
		})),
		initialValue: defaultAccountId
	})) ?? defaultAccountId;
}
async function collectChannelStatus(params) {
	const installedPlugins = listChannelPlugins();
	const installedIds = new Set(installedPlugins.map((plugin) => plugin.id));
	const catalogEntries = listChannelPluginCatalogEntries({ workspaceDir: resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg)) }).filter((entry) => !installedIds.has(entry.id));
	const statusEntries = await Promise.all(listChannelOnboardingAdapters().map((adapter) => adapter.getStatus({
		cfg: params.cfg,
		options: params.options,
		accountOverrides: params.accountOverrides
	})));
	const statusByChannel = new Map(statusEntries.map((entry) => [entry.channel, entry]));
	const fallbackStatuses = listChatChannels().filter((meta) => !statusByChannel.has(meta.id)).map((meta) => {
		const configured = isChannelConfigured(params.cfg, meta.id);
		const statusLabel = configured ? "configured (plugin disabled)" : "not configured";
		return {
			channel: meta.id,
			configured,
			statusLines: [`${meta.label}: ${statusLabel}`],
			selectionHint: configured ? "configured · plugin disabled" : "not configured",
			quickstartScore: 0
		};
	});
	const catalogStatuses = catalogEntries.map((entry) => ({
		channel: entry.id,
		configured: false,
		statusLines: [`${entry.meta.label}: install plugin to enable`],
		selectionHint: "plugin · install",
		quickstartScore: 0
	}));
	const combinedStatuses = [
		...statusEntries,
		...fallbackStatuses,
		...catalogStatuses
	];
	return {
		installedPlugins,
		catalogEntries,
		statusByChannel: new Map(combinedStatuses.map((entry) => [entry.channel, entry])),
		statusLines: combinedStatuses.flatMap((entry) => entry.statusLines)
	};
}
async function noteChannelStatus(params) {
	const { statusLines } = await collectChannelStatus({
		cfg: params.cfg,
		options: params.options,
		accountOverrides: params.accountOverrides ?? {}
	});
	if (statusLines.length > 0) await params.prompter.note(statusLines.join("\n"), "Channel status");
}
async function noteChannelPrimer(prompter, channels) {
	const channelLines = channels.map((channel) => formatChannelPrimerLine({
		id: channel.id,
		label: channel.label,
		selectionLabel: channel.label,
		docsPath: "/",
		blurb: channel.blurb
	}));
	await prompter.note([
		"DM security: default is pairing; unknown DMs get a pairing code.",
		`Approve with: ${formatCliCommand("symi pairing approve <channel> <code>")}`,
		"Public DMs require dmPolicy=\"open\" + allowFrom=[\"*\"].",
		"Multi-user DMs: run: " + formatCliCommand("symi config set session.dmScope \"per-channel-peer\"") + " (or \"per-account-channel-peer\" for multi-account channels) to isolate sessions.",
		`Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`,
		"",
		...channelLines
	].join("\n"), "How channels work");
}
function resolveQuickstartDefault(statusByChannel) {
	let best = null;
	for (const [channel, status] of statusByChannel) {
		if (status.quickstartScore == null) continue;
		if (!best || status.quickstartScore > best.score) best = {
			channel,
			score: status.quickstartScore
		};
	}
	return best?.channel;
}
async function maybeConfigureDmPolicies(params) {
	const { selection, prompter, accountIdsByChannel } = params;
	const dmPolicies = selection.map((channel) => getChannelOnboardingAdapter(channel)?.dmPolicy).filter(Boolean);
	if (dmPolicies.length === 0) return params.cfg;
	if (!await prompter.confirm({
		message: "Configure DM access policies now? (default: pairing)",
		initialValue: false
	})) return params.cfg;
	let cfg = params.cfg;
	const selectPolicy = async (policy) => {
		await prompter.note([
			"Default: pairing (unknown DMs get a pairing code).",
			`Approve: ${formatCliCommand(`symi pairing approve ${policy.channel} <code>`)}`,
			`Allowlist DMs: ${policy.policyKey}="allowlist" + ${policy.allowFromKey} entries.`,
			`Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`,
			"Multi-user DMs: run: " + formatCliCommand("symi config set session.dmScope \"per-channel-peer\"") + " (or \"per-account-channel-peer\" for multi-account channels) to isolate sessions.",
			`Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`
		].join("\n"), `${policy.label} DM access`);
		return await prompter.select({
			message: `${policy.label} DM policy`,
			options: [
				{
					value: "pairing",
					label: "Pairing (recommended)"
				},
				{
					value: "allowlist",
					label: "Allowlist (specific users only)"
				},
				{
					value: "open",
					label: "Open (public inbound DMs)"
				},
				{
					value: "disabled",
					label: "Disabled (ignore DMs)"
				}
			]
		});
	};
	for (const policy of dmPolicies) {
		const current = policy.getCurrent(cfg);
		const nextPolicy = await selectPolicy(policy);
		if (nextPolicy !== current) cfg = policy.setPolicy(cfg, nextPolicy);
		if (nextPolicy === "allowlist" && policy.promptAllowFrom) cfg = await policy.promptAllowFrom({
			cfg,
			prompter,
			accountId: accountIdsByChannel?.get(policy.channel)
		});
	}
	return cfg;
}
async function setupChannels(cfg, runtime, prompter, options) {
	let next = cfg;
	const forceAllowFromChannels = new Set(options?.forceAllowFromChannels ?? []);
	const accountOverrides = { ...options?.accountIds };
	if (options?.whatsappAccountId?.trim()) accountOverrides.whatsapp = options.whatsappAccountId.trim();
	const { installedPlugins, catalogEntries, statusByChannel, statusLines } = await collectChannelStatus({
		cfg: next,
		options,
		accountOverrides
	});
	if (!options?.skipStatusNote && statusLines.length > 0) await prompter.note(statusLines.join("\n"), "Channel status");
	if (!(options?.skipConfirm ? true : await prompter.confirm({
		message: "Configure chat channels now?",
		initialValue: true
	}))) return cfg;
	const corePrimer = listChatChannels().map((meta) => ({
		id: meta.id,
		label: meta.label,
		blurb: meta.blurb
	}));
	const coreIds = new Set(corePrimer.map((entry) => entry.id));
	await noteChannelPrimer(prompter, [
		...corePrimer,
		...installedPlugins.filter((plugin) => !coreIds.has(plugin.id)).map((plugin) => ({
			id: plugin.id,
			label: plugin.meta.label,
			blurb: plugin.meta.blurb
		})),
		...catalogEntries.filter((entry) => !coreIds.has(entry.id)).map((entry) => ({
			id: entry.id,
			label: entry.meta.label,
			blurb: entry.meta.blurb
		}))
	]);
	const quickstartDefault = options?.initialSelection?.[0] ?? resolveQuickstartDefault(statusByChannel);
	const shouldPromptAccountIds = options?.promptAccountIds === true;
	const accountIdsByChannel = /* @__PURE__ */ new Map();
	const recordAccount = (channel, accountId) => {
		options?.onAccountId?.(channel, accountId);
		getChannelOnboardingAdapter(channel)?.onAccountRecorded?.(accountId, options);
		accountIdsByChannel.set(channel, accountId);
	};
	const selection = [];
	const addSelection = (channel) => {
		if (!selection.includes(channel)) selection.push(channel);
	};
	const resolveDisabledHint = (channel) => {
		const plugin = getChannelPlugin(channel);
		if (!plugin) {
			if (next.plugins?.entries?.[channel]?.enabled === false) return "plugin disabled";
			if (next.plugins?.enabled === false) return "plugins disabled";
			return;
		}
		const accountId = resolveChannelDefaultAccountId({
			plugin,
			cfg: next
		});
		const account = plugin.config.resolveAccount(next, accountId);
		let enabled;
		if (plugin.config.isEnabled) enabled = plugin.config.isEnabled(account, next);
		else if (typeof account?.enabled === "boolean") enabled = account.enabled;
		else if (typeof next.channels?.[channel]?.enabled === "boolean") enabled = next.channels[channel]?.enabled;
		return enabled === false ? "disabled" : void 0;
	};
	const buildSelectionOptions = (entries) => entries.map((entry) => {
		const status = statusByChannel.get(entry.id);
		const disabledHint = resolveDisabledHint(entry.id);
		const hint = [status?.selectionHint, disabledHint].filter(Boolean).join(" · ") || void 0;
		return {
			value: entry.meta.id,
			label: entry.meta.selectionLabel ?? entry.meta.label,
			...hint ? { hint } : {}
		};
	});
	const getChannelEntries = () => {
		const core = listChatChannels();
		const installed = listChannelPlugins();
		const installedIds = new Set(installed.map((plugin) => plugin.id));
		const catalog = listChannelPluginCatalogEntries({ workspaceDir: resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next)) }).filter((entry) => !installedIds.has(entry.id));
		const metaById = /* @__PURE__ */ new Map();
		for (const meta of core) metaById.set(meta.id, meta);
		for (const plugin of installed) metaById.set(plugin.id, plugin.meta);
		for (const entry of catalog) if (!metaById.has(entry.id)) metaById.set(entry.id, entry.meta);
		return {
			entries: Array.from(metaById, ([id, meta]) => ({
				id,
				meta
			})),
			catalog,
			catalogById: new Map(catalog.map((entry) => [entry.id, entry]))
		};
	};
	const refreshStatus = async (channel) => {
		const adapter = getChannelOnboardingAdapter(channel);
		if (!adapter) return;
		const status = await adapter.getStatus({
			cfg: next,
			options,
			accountOverrides
		});
		statusByChannel.set(channel, status);
	};
	const ensureBundledPluginEnabled = async (channel) => {
		if (getChannelPlugin(channel)) return true;
		const result = enablePluginInConfig(next, channel);
		next = result.config;
		if (!result.enabled) {
			await prompter.note(`Cannot enable ${channel}: ${result.reason ?? "plugin disabled"}.`, "Channel setup");
			return false;
		}
		const workspaceDir = resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next));
		reloadOnboardingPluginRegistry({
			cfg: next,
			runtime,
			workspaceDir
		});
		if (!getChannelPlugin(channel)) {
			await prompter.note(`${channel} plugin not available.`, "Channel setup");
			return false;
		}
		await refreshStatus(channel);
		return true;
	};
	const configureChannel = async (channel) => {
		const adapter = getChannelOnboardingAdapter(channel);
		if (!adapter) {
			await prompter.note(`${channel} does not support onboarding yet.`, "Channel setup");
			return;
		}
		const result = await adapter.configure({
			cfg: next,
			runtime,
			prompter,
			options,
			accountOverrides,
			shouldPromptAccountIds,
			forceAllowFrom: forceAllowFromChannels.has(channel)
		});
		next = result.cfg;
		if (result.accountId) recordAccount(channel, result.accountId);
		addSelection(channel);
		await refreshStatus(channel);
	};
	const handleConfiguredChannel = async (channel, label) => {
		const plugin = getChannelPlugin(channel);
		const adapter = getChannelOnboardingAdapter(channel);
		const supportsDisable = Boolean(options?.allowDisable && (plugin?.config.setAccountEnabled || adapter?.disable));
		const supportsDelete = Boolean(options?.allowDisable && plugin?.config.deleteAccount);
		const action = await promptConfiguredAction({
			prompter,
			label,
			supportsDisable,
			supportsDelete
		});
		if (action === "skip") return;
		if (action === "update") {
			await configureChannel(channel);
			return;
		}
		if (!options?.allowDisable) return;
		if (action === "delete" && !supportsDelete) {
			await prompter.note(`${label} does not support deleting config entries.`, "Remove channel");
			return;
		}
		const resolvedAccountId = normalizeAccountId((action === "delete" ? Boolean(plugin?.config.deleteAccount) : Boolean(plugin?.config.setAccountEnabled)) ? await promptRemovalAccountId({
			cfg: next,
			prompter,
			label,
			channel
		}) : DEFAULT_ACCOUNT_ID) ?? (plugin ? resolveChannelDefaultAccountId({
			plugin,
			cfg: next
		}) : DEFAULT_ACCOUNT_ID);
		const accountLabel = formatAccountLabel(resolvedAccountId);
		if (action === "delete") {
			if (!await prompter.confirm({
				message: `Delete ${label} account "${accountLabel}"?`,
				initialValue: false
			})) return;
			if (plugin?.config.deleteAccount) next = plugin.config.deleteAccount({
				cfg: next,
				accountId: resolvedAccountId
			});
			await refreshStatus(channel);
			return;
		}
		if (plugin?.config.setAccountEnabled) next = plugin.config.setAccountEnabled({
			cfg: next,
			accountId: resolvedAccountId,
			enabled: false
		});
		else if (adapter?.disable) next = adapter.disable(next);
		await refreshStatus(channel);
	};
	const handleChannelChoice = async (channel) => {
		const { catalogById } = getChannelEntries();
		const catalogEntry = catalogById.get(channel);
		if (catalogEntry) {
			const workspaceDir = resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next));
			const result = await ensureOnboardingPluginInstalled({
				cfg: next,
				entry: catalogEntry,
				prompter,
				runtime,
				workspaceDir
			});
			next = result.cfg;
			if (!result.installed) return;
			reloadOnboardingPluginRegistry({
				cfg: next,
				runtime,
				workspaceDir
			});
			await refreshStatus(channel);
		} else if (!await ensureBundledPluginEnabled(channel)) return;
		const label = getChannelPlugin(channel)?.meta.label ?? catalogEntry?.meta.label ?? channel;
		if (statusByChannel.get(channel)?.configured ?? false) {
			await handleConfiguredChannel(channel, label);
			return;
		}
		await configureChannel(channel);
	};
	if (options?.quickstartDefaults) {
		const { entries } = getChannelEntries();
		const choice = await prompter.select({
			message: "Select channel (QuickStart)",
			options: [...buildSelectionOptions(entries), {
				value: "__skip__",
				label: "Skip for now",
				hint: `You can add channels later via \`${formatCliCommand("symi channels add")}\``
			}],
			initialValue: quickstartDefault
		});
		if (choice !== "__skip__") await handleChannelChoice(choice);
	} else {
		const doneValue = "__done__";
		const initialValue = options?.initialSelection?.[0] ?? quickstartDefault;
		while (true) {
			const { entries } = getChannelEntries();
			const choice = await prompter.select({
				message: "Select a channel",
				options: [...buildSelectionOptions(entries), {
					value: doneValue,
					label: "Finished",
					hint: selection.length > 0 ? "Done" : "Skip for now"
				}],
				initialValue
			});
			if (choice === doneValue) break;
			await handleChannelChoice(choice);
		}
	}
	options?.onSelection?.(selection);
	const selectionNotes = /* @__PURE__ */ new Map();
	const { entries: selectionEntries } = getChannelEntries();
	for (const entry of selectionEntries) selectionNotes.set(entry.id, formatChannelSelectionLine(entry.meta, formatDocsLink));
	const selectedLines = selection.map((channel) => selectionNotes.get(channel)).filter((line) => Boolean(line));
	if (selectedLines.length > 0) await prompter.note(selectedLines.join("\n"), "Selected channels");
	if (!options?.skipDmPolicyPrompt) next = await maybeConfigureDmPolicies({
		cfg: next,
		selection,
		prompter,
		accountIdsByChannel
	});
	return next;
}

//#endregion
export { reloadOnboardingPluginRegistry as a, ensureOnboardingPluginInstalled as i, onboard_channels_exports as n, setupChannels as r, noteChannelStatus as t };