Split agent setup editor helpers by editor

This commit is contained in:
OpenPets Dev 2026-06-19 21:59:38 +00:00
parent a42efdeea9
commit 116d312cca
6 changed files with 377 additions and 339 deletions

View file

@ -0,0 +1,180 @@
import { buildCursorRulesPreview, buildFamiliarOSOnlyPreview, classifyCursorMcpStatus, executeCursorMcpWrite, getCursorGlobalMcpPath, planCursorMcpInstall, planCursorMcpRemove, planCursorMcpReplace, readCursorMcpConfig, type CursorMcpStatusResult, type RedactedPreview } from "@familiaros/cursor";
export interface CursorSetupStatus {
readonly state: "configured" | "needs_setup" | "not_detected" | "error" | "conflict" | "needs_update";
readonly label: string;
readonly details: string;
readonly configPath: string;
readonly canInstall: boolean;
readonly canReplace: boolean;
readonly canRemove: boolean;
}
export interface CursorSetupPreview {
readonly global: true;
readonly configPath: string;
readonly mcpEntry: RedactedPreview;
readonly rulesPath: string;
readonly rulesContent: string;
readonly commandMode: "published" | "local" | "bundled";
}
type CursorActionResult = {
readonly ok: boolean;
readonly action: "cursor-install" | "cursor-replace" | "cursor-remove";
readonly message: string;
readonly changed: boolean;
};
export function buildCursorSetupSnapshot({
homeDir,
selectedPetId,
mcpVersion,
formatUserPath,
}: {
readonly homeDir: string;
readonly selectedPetId: string | undefined;
readonly mcpVersion: string;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): { readonly status: CursorSetupStatus; readonly preview: CursorSetupPreview } {
const configPath = getCursorGlobalMcpPath(homeDir);
const petId = selectedPetId || undefined;
const configResult = readCursorMcpConfig(configPath);
const statusResult = classifyCursorMcpStatus(configResult, configPath, { mcpVersion, petId, commandMode: "published" });
return {
status: {
state: mapCursorStatusToState(statusResult.status),
label: mapCursorStatusToLabel(statusResult.status),
details: statusResult.message,
configPath: formatUserPath(configPath) ?? configPath,
canInstall: statusResult.canInstall,
canReplace: statusResult.canReplace,
canRemove: statusResult.canRemove,
},
preview: {
global: true,
configPath: formatUserPath(configPath) ?? configPath,
mcpEntry: buildFamiliarOSOnlyPreview({ mcpVersion, petId, commandMode: "published" }),
rulesPath: ".cursor/rules/familiaros.mdc",
rulesContent: buildCursorRulesPreview(),
commandMode: "published",
},
};
}
export function installCursorGlobalConfig({
homeDir,
selectedPetId,
mcpVersion,
formatUserPath,
}: {
readonly homeDir: string;
readonly selectedPetId: string | undefined;
readonly mcpVersion: string;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): CursorActionResult {
try {
const configPath = getCursorGlobalMcpPath(homeDir);
const plan = planCursorMcpInstall(configPath, { mcpVersion, petId: selectedPetId || undefined, commandMode: "published" });
if ("ok" in plan && !plan.ok) {
return { ok: false, action: "cursor-install", message: plan.message, changed: false };
}
if ("targetPath" in plan) {
executeCursorMcpWrite(plan);
const backupMsg = plan.backupPath ? ` Backup: ${formatUserPath(plan.backupPath) ?? plan.backupPath}.` : "";
return { ok: true, action: "cursor-install", message: `Installed Cursor FamiliarOS MCP config at ${formatUserPath(configPath) ?? configPath}.${backupMsg} Cursor may need to be restarted or reloaded.`, changed: true };
}
return { ok: false, action: "cursor-install", message: "Failed to plan Cursor MCP install.", changed: false };
} catch (error) {
return { ok: false, action: "cursor-install", message: error instanceof Error ? error.message : "Cursor MCP install failed.", changed: false };
}
}
export function replaceCursorGlobalConfig({
homeDir,
selectedPetId,
mcpVersion,
formatUserPath,
}: {
readonly homeDir: string;
readonly selectedPetId: string | undefined;
readonly mcpVersion: string;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): CursorActionResult {
try {
const configPath = getCursorGlobalMcpPath(homeDir);
const plan = planCursorMcpReplace(configPath, { mcpVersion, petId: selectedPetId || undefined, commandMode: "published" });
if ("ok" in plan && !plan.ok) {
return { ok: false, action: "cursor-replace", message: plan.message, changed: false };
}
if ("targetPath" in plan) {
executeCursorMcpWrite(plan);
const backupMsg = plan.backupPath ? ` Backup: ${formatUserPath(plan.backupPath) ?? plan.backupPath}.` : "";
return { ok: true, action: "cursor-replace", message: `Replaced Cursor FamiliarOS MCP config at ${formatUserPath(configPath) ?? configPath}.${backupMsg} Cursor may need to be restarted or reloaded.`, changed: true };
}
return { ok: false, action: "cursor-replace", message: "Failed to plan Cursor MCP replace.", changed: false };
} catch (error) {
return { ok: false, action: "cursor-replace", message: error instanceof Error ? error.message : "Cursor MCP replace failed.", changed: false };
}
}
export function removeCursorGlobalConfig({
homeDir,
formatUserPath,
}: {
readonly homeDir: string;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): CursorActionResult {
try {
const configPath = getCursorGlobalMcpPath(homeDir);
const plan = planCursorMcpRemove(configPath);
if ("ok" in plan && !plan.ok) {
return { ok: false, action: "cursor-remove", message: plan.message, changed: false };
}
if ("targetPath" in plan) {
executeCursorMcpWrite(plan);
return { ok: true, action: "cursor-remove", message: `Removed Cursor FamiliarOS MCP config at ${formatUserPath(configPath) ?? configPath}. Cursor may need to be restarted or reloaded.`, changed: true };
}
return { ok: false, action: "cursor-remove", message: "Failed to plan Cursor MCP remove.", changed: false };
} catch (error) {
return { ok: false, action: "cursor-remove", message: error instanceof Error ? error.message : "Cursor MCP remove failed.", changed: false };
}
}
function mapCursorStatusToState(status: CursorMcpStatusResult["status"]): CursorSetupStatus["state"] {
switch (status) {
case "installed":
return "configured";
case "missing":
return "needs_setup";
case "needs-update":
return "needs_update";
case "conflict":
return "conflict";
case "invalid":
case "error":
return "error";
default:
return "error";
}
}
function mapCursorStatusToLabel(status: CursorMcpStatusResult["status"]): string {
switch (status) {
case "installed":
return "Configured";
case "missing":
return "Not configured";
case "needs-update":
return "Needs update";
case "conflict":
return "Conflict";
case "invalid":
case "error":
return "Config error";
default:
return "Checking";
}
}

View file

@ -0,0 +1,152 @@
import { doctorOpenCodeGlobalSetup, getGlobalOpenCodeConfigDir, parseOpenCodeConfig, prepareOpenCodeGlobalRemove, prepareOpenCodeGlobalSetup, writePreparedOpenCodeGlobalRemove, writePreparedOpenCodeGlobalSetup } from "@familiaros/opencode";
import type { FamiliarOSCommandMode } from "@familiaros/claude";
export interface OpenCodeSetupStatus {
readonly state: "configured" | "needs_setup" | "not_detected" | "error";
readonly label: string;
readonly details: string;
readonly configDir: string;
readonly canInstall: boolean;
readonly canRemove: boolean;
}
export interface OpenCodeSetupPreview {
readonly global: true;
readonly configDir: string;
readonly configPath: string;
readonly cleanupConfigPaths: readonly string[];
readonly mcpCommand: readonly string[];
readonly plugin: readonly unknown[] | string;
readonly instructionPath: string;
readonly configPreview: Record<string, unknown>;
}
type OpenCodeActionResult = {
readonly ok: boolean;
readonly action: "opencode-install" | "opencode-remove";
readonly message: string;
readonly changed: boolean;
};
export function buildOpenCodeSetupSnapshot({
env,
homeDir,
processPlatform,
selectedPetId,
commandMode,
cliVersion,
pluginVersion,
cliEntryPath,
detectedOk,
preferredOpenCodeCommand,
formatUserPath,
}: {
readonly env: NodeJS.ProcessEnv;
readonly homeDir: string;
readonly processPlatform: NodeJS.Platform;
readonly selectedPetId: string | undefined;
readonly commandMode: FamiliarOSCommandMode;
readonly cliVersion: string;
readonly pluginVersion: string;
readonly cliEntryPath: string | undefined;
readonly detectedOk: boolean;
readonly preferredOpenCodeCommand: string;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): { readonly status: OpenCodeSetupStatus; readonly preview: OpenCodeSetupPreview } {
const configDir = getGlobalOpenCodeConfigDir(env, homeDir, processPlatform);
const petId = selectedPetId || undefined;
const prepared = safePrepareOpenCode(configDir, petId, cliVersion, pluginVersion, commandMode, cliEntryPath);
const globalState = doctorOpenCodeGlobalSetup(configDir);
const configured = globalState.status === "installed";
return {
status: {
state: globalState.status === "error" || globalState.status === "custom" || globalState.status === "conflict" ? "error" : configured ? "configured" : detectedOk ? "needs_setup" : "not_detected",
label: configured ? "Installed" : globalState.status === "custom" || globalState.status === "conflict" ? "Needs attention" : detectedOk ? "Ready" : "Not detected",
details: globalState.status === "custom" || globalState.status === "conflict" || globalState.status === "error" ? globalState.message : configured ? globalState.message : detectedOk ? "OpenCode was detected. Desktop setup writes global OpenCode config." : preferredOpenCodeCommand === (processPlatform === "win32" ? "opencode.cmd" : "opencode") ? "OpenCode was not found on PATH. You can still preview setup, but OpenCode must be installed to use it." : "OpenCode did not run from the saved command path. You can still preview setup, but OpenCode must be installed to use it.",
configDir: formatUserPath(configDir) ?? configDir,
canInstall: prepared.ok && !configured,
canRemove: configured,
},
preview: {
global: true,
configDir: formatUserPath(configDir) ?? configDir,
configPath: prepared.ok ? (formatUserPath(prepared.configPath) ?? prepared.configPath) : "",
cleanupConfigPaths: prepared.ok ? prepared.cleanupConfigPaths.map((path) => formatUserPath(path) ?? path) : [],
mcpCommand: prepared.ok ? prepared.command : [],
plugin: prepared.ok ? prepared.plugin : (petId ? [`@familiaros/opencode@${pluginVersion}`, { familiar: petId }] : `@familiaros/opencode@${pluginVersion}`),
instructionPath: prepared.ok ? (formatUserPath(prepared.instructionPath) ?? prepared.instructionPath) : "",
configPreview: prepared.ok ? prepared.configPreview : {},
},
};
}
export function installOpenCodeGlobalConfig({
env,
homeDir,
processPlatform,
selectedPetId,
commandMode,
cliVersion,
pluginVersion,
cliEntryPath,
formatUserPath,
}: {
readonly env: NodeJS.ProcessEnv;
readonly homeDir: string;
readonly processPlatform: NodeJS.Platform;
readonly selectedPetId: string | undefined;
readonly commandMode: FamiliarOSCommandMode;
readonly cliVersion: string;
readonly pluginVersion: string;
readonly cliEntryPath: string | undefined;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): OpenCodeActionResult {
try {
const configDir = getGlobalOpenCodeConfigDir(env, homeDir, processPlatform);
const prepared = prepareOpenCodeGlobalSetup({ configDir, petId: selectedPetId || undefined, cliVersion, pluginVersion, commandMode, cliEntryPath });
writePreparedOpenCodeGlobalSetup(prepared);
return { ok: true, action: "opencode-install", message: `Installed global OpenCode FamiliarOS setup. Config: ${formatUserPath(prepared.configPath) ?? prepared.configPath}. Instructions: ${formatUserPath(prepared.instructionPath) ?? prepared.instructionPath}.`, changed: true };
} catch (error) {
return { ok: false, action: "opencode-install", message: error instanceof Error ? error.message : "OpenCode setup failed.", changed: false };
}
}
export function removeOpenCodeGlobalConfig({
env,
homeDir,
processPlatform,
}: {
readonly env: NodeJS.ProcessEnv;
readonly homeDir: string;
readonly processPlatform: NodeJS.Platform;
}): OpenCodeActionResult {
try {
const configDir = getGlobalOpenCodeConfigDir(env, homeDir, processPlatform);
const prepared = prepareOpenCodeGlobalRemove(configDir);
writePreparedOpenCodeGlobalRemove(prepared);
return { ok: true, action: "opencode-remove", message: prepared.configWrites.length > 0 ? "Removed global OpenCode FamiliarOS setup." : "Global OpenCode FamiliarOS setup was already absent.", changed: prepared.configWrites.length > 0 };
} catch (error) {
return { ok: false, action: "opencode-remove", message: error instanceof Error ? error.message : "OpenCode removal failed.", changed: false };
}
}
function safePrepareOpenCode(
configDir: string,
selectedPetId: string | undefined,
cliVersion: string,
pluginVersion: string,
commandMode: FamiliarOSCommandMode,
cliEntryPath: string | undefined,
): { readonly ok: true; readonly command: readonly string[]; readonly configPath: string; readonly cleanupConfigPaths: readonly string[]; readonly instructionPath: string; readonly plugin: readonly unknown[] | string; readonly configPreview: Record<string, unknown> } | { readonly ok: false; readonly message: string } {
try {
const prepared = prepareOpenCodeGlobalSetup({ configDir, petId: selectedPetId || undefined, cliVersion, pluginVersion, commandMode, cliEntryPath });
const parsed = parseOpenCodeConfig(prepared.configWrite.content);
if (!parsed.ok) return { ok: false, message: parsed.message };
const config = parsed.value as { mcp?: { familiaros?: { command?: readonly string[] } }; plugin?: readonly unknown[] };
const plugin = Array.isArray(config.plugin) ? config.plugin[config.plugin.length - 1] : undefined;
return { ok: true, command: config.mcp?.familiaros?.command ?? [], configPath: prepared.configPath, cleanupConfigPaths: prepared.cleanupConfigWrites.map((write) => write.targetPath), instructionPath: prepared.instructionPath, plugin: plugin === undefined ? [] : (plugin as readonly unknown[] | string), configPreview: parsed.value };
} catch (error) {
return { ok: false, message: error instanceof Error ? error.message : "OpenCode setup preview failed." };
}
}

View file

@ -1,332 +1,19 @@
import { buildCursorRulesPreview, buildFamiliarOSOnlyPreview, classifyCursorMcpStatus, executeCursorMcpWrite, getCursorGlobalMcpPath, planCursorMcpInstall, planCursorMcpRemove, planCursorMcpReplace, readCursorMcpConfig, type CursorMcpStatusResult, type RedactedPreview } from "@familiaros/cursor";
import { doctorOpenCodeGlobalSetup, getGlobalOpenCodeConfigDir, parseOpenCodeConfig, prepareOpenCodeGlobalRemove, prepareOpenCodeGlobalSetup, writePreparedOpenCodeGlobalRemove, writePreparedOpenCodeGlobalSetup } from "@familiaros/opencode";
import type { FamiliarOSCommandMode } from "@familiaros/claude";
export interface OpenCodeSetupStatus {
readonly state: "configured" | "needs_setup" | "not_detected" | "error";
readonly label: string;
readonly details: string;
readonly configDir: string;
readonly canInstall: boolean;
readonly canRemove: boolean;
}
export interface OpenCodeSetupPreview {
readonly global: true;
readonly configDir: string;
readonly configPath: string;
readonly cleanupConfigPaths: readonly string[];
readonly mcpCommand: readonly string[];
readonly plugin: readonly unknown[] | string;
readonly instructionPath: string;
readonly configPreview: Record<string, unknown>;
}
export interface CursorSetupStatus {
readonly state: "configured" | "needs_setup" | "not_detected" | "error" | "conflict" | "needs_update";
readonly label: string;
readonly details: string;
readonly configPath: string;
readonly canInstall: boolean;
readonly canReplace: boolean;
readonly canRemove: boolean;
}
export interface CursorSetupPreview {
readonly global: true;
readonly configPath: string;
readonly mcpEntry: RedactedPreview;
readonly rulesPath: string;
readonly rulesContent: string;
readonly commandMode: "published" | "local" | "bundled";
}
type OpenCodeActionResult = {
readonly ok: boolean;
readonly action: "opencode-install" | "opencode-remove";
readonly message: string;
readonly changed: boolean;
};
type CursorActionResult = {
readonly ok: boolean;
readonly action: "cursor-install" | "cursor-replace" | "cursor-remove";
readonly message: string;
readonly changed: boolean;
};
export function buildOpenCodeSetupSnapshot({
env,
homeDir,
processPlatform,
selectedPetId,
commandMode,
cliVersion,
pluginVersion,
cliEntryPath,
detectedOk,
preferredOpenCodeCommand,
formatUserPath,
}: {
readonly env: NodeJS.ProcessEnv;
readonly homeDir: string;
readonly processPlatform: NodeJS.Platform;
readonly selectedPetId: string | undefined;
readonly commandMode: FamiliarOSCommandMode;
readonly cliVersion: string;
readonly pluginVersion: string;
readonly cliEntryPath: string | undefined;
readonly detectedOk: boolean;
readonly preferredOpenCodeCommand: string;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): { readonly status: OpenCodeSetupStatus; readonly preview: OpenCodeSetupPreview } {
const configDir = getGlobalOpenCodeConfigDir(env, homeDir, processPlatform);
const petId = selectedPetId || undefined;
const prepared = safePrepareOpenCode(configDir, petId, cliVersion, pluginVersion, commandMode, cliEntryPath);
const globalState = doctorOpenCodeGlobalSetup(configDir);
const configured = globalState.status === "installed";
return {
status: {
state: globalState.status === "error" || globalState.status === "custom" || globalState.status === "conflict" ? "error" : configured ? "configured" : detectedOk ? "needs_setup" : "not_detected",
label: configured ? "Installed" : globalState.status === "custom" || globalState.status === "conflict" ? "Needs attention" : detectedOk ? "Ready" : "Not detected",
details: globalState.status === "custom" || globalState.status === "conflict" || globalState.status === "error" ? globalState.message : configured ? globalState.message : detectedOk ? "OpenCode was detected. Desktop setup writes global OpenCode config." : preferredOpenCodeCommand === (processPlatform === "win32" ? "opencode.cmd" : "opencode") ? "OpenCode was not found on PATH. You can still preview setup, but OpenCode must be installed to use it." : "OpenCode did not run from the saved command path. You can still preview setup, but OpenCode must be installed to use it.",
configDir: formatUserPath(configDir) ?? configDir,
canInstall: prepared.ok && !configured,
canRemove: configured,
},
preview: {
global: true,
configDir: formatUserPath(configDir) ?? configDir,
configPath: prepared.ok ? (formatUserPath(prepared.configPath) ?? prepared.configPath) : "",
cleanupConfigPaths: prepared.ok ? prepared.cleanupConfigPaths.map((path) => formatUserPath(path) ?? path) : [],
mcpCommand: prepared.ok ? prepared.command : [],
plugin: prepared.ok ? prepared.plugin : (petId ? [`@familiaros/opencode@${pluginVersion}`, { familiar: petId }] : `@familiaros/opencode@${pluginVersion}`),
instructionPath: prepared.ok ? (formatUserPath(prepared.instructionPath) ?? prepared.instructionPath) : "",
configPreview: prepared.ok ? prepared.configPreview : {},
},
};
}
export function buildCursorSetupSnapshot({
homeDir,
selectedPetId,
mcpVersion,
formatUserPath,
}: {
readonly homeDir: string;
readonly selectedPetId: string | undefined;
readonly mcpVersion: string;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): { readonly status: CursorSetupStatus; readonly preview: CursorSetupPreview } {
const configPath = getCursorGlobalMcpPath(homeDir);
const petId = selectedPetId || undefined;
const configResult = readCursorMcpConfig(configPath);
const statusResult = classifyCursorMcpStatus(configResult, configPath, { mcpVersion, petId, commandMode: "published" });
return {
status: {
state: mapCursorStatusToState(statusResult.status),
label: mapCursorStatusToLabel(statusResult.status),
details: statusResult.message,
configPath: formatUserPath(configPath) ?? configPath,
canInstall: statusResult.canInstall,
canReplace: statusResult.canReplace,
canRemove: statusResult.canRemove,
},
preview: {
global: true,
configPath: formatUserPath(configPath) ?? configPath,
mcpEntry: buildFamiliarOSOnlyPreview({ mcpVersion, petId, commandMode: "published" }),
rulesPath: ".cursor/rules/familiaros.mdc",
rulesContent: buildCursorRulesPreview(),
commandMode: "published",
},
};
}
export function installOpenCodeGlobalConfig({
env,
homeDir,
processPlatform,
selectedPetId,
commandMode,
cliVersion,
pluginVersion,
cliEntryPath,
formatUserPath,
}: {
readonly env: NodeJS.ProcessEnv;
readonly homeDir: string;
readonly processPlatform: NodeJS.Platform;
readonly selectedPetId: string | undefined;
readonly commandMode: FamiliarOSCommandMode;
readonly cliVersion: string;
readonly pluginVersion: string;
readonly cliEntryPath: string | undefined;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): OpenCodeActionResult {
try {
const configDir = getGlobalOpenCodeConfigDir(env, homeDir, processPlatform);
const prepared = prepareOpenCodeGlobalSetup({ configDir, petId: selectedPetId || undefined, cliVersion, pluginVersion, commandMode, cliEntryPath });
writePreparedOpenCodeGlobalSetup(prepared);
return { ok: true, action: "opencode-install", message: `Installed global OpenCode FamiliarOS setup. Config: ${formatUserPath(prepared.configPath) ?? prepared.configPath}. Instructions: ${formatUserPath(prepared.instructionPath) ?? prepared.instructionPath}.`, changed: true };
} catch (error) {
return { ok: false, action: "opencode-install", message: error instanceof Error ? error.message : "OpenCode setup failed.", changed: false };
}
}
export function removeOpenCodeGlobalConfig({
env,
homeDir,
processPlatform,
}: {
readonly env: NodeJS.ProcessEnv;
readonly homeDir: string;
readonly processPlatform: NodeJS.Platform;
}): OpenCodeActionResult {
try {
const configDir = getGlobalOpenCodeConfigDir(env, homeDir, processPlatform);
const prepared = prepareOpenCodeGlobalRemove(configDir);
writePreparedOpenCodeGlobalRemove(prepared);
return { ok: true, action: "opencode-remove", message: prepared.configWrites.length > 0 ? "Removed global OpenCode FamiliarOS setup." : "Global OpenCode FamiliarOS setup was already absent.", changed: prepared.configWrites.length > 0 };
} catch (error) {
return { ok: false, action: "opencode-remove", message: error instanceof Error ? error.message : "OpenCode removal failed.", changed: false };
}
}
export function installCursorGlobalConfig({
homeDir,
selectedPetId,
mcpVersion,
formatUserPath,
}: {
readonly homeDir: string;
readonly selectedPetId: string | undefined;
readonly mcpVersion: string;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): CursorActionResult {
try {
const configPath = getCursorGlobalMcpPath(homeDir);
const plan = planCursorMcpInstall(configPath, { mcpVersion, petId: selectedPetId || undefined, commandMode: "published" });
if ("ok" in plan && !plan.ok) {
return { ok: false, action: "cursor-install", message: plan.message, changed: false };
}
if ("targetPath" in plan) {
executeCursorMcpWrite(plan);
const backupMsg = plan.backupPath ? ` Backup: ${formatUserPath(plan.backupPath) ?? plan.backupPath}.` : "";
return { ok: true, action: "cursor-install", message: `Installed Cursor FamiliarOS MCP config at ${formatUserPath(configPath) ?? configPath}.${backupMsg} Cursor may need to be restarted or reloaded.`, changed: true };
}
return { ok: false, action: "cursor-install", message: "Failed to plan Cursor MCP install.", changed: false };
} catch (error) {
return { ok: false, action: "cursor-install", message: error instanceof Error ? error.message : "Cursor MCP install failed.", changed: false };
}
}
export function replaceCursorGlobalConfig({
homeDir,
selectedPetId,
mcpVersion,
formatUserPath,
}: {
readonly homeDir: string;
readonly selectedPetId: string | undefined;
readonly mcpVersion: string;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): CursorActionResult {
try {
const configPath = getCursorGlobalMcpPath(homeDir);
const plan = planCursorMcpReplace(configPath, { mcpVersion, petId: selectedPetId || undefined, commandMode: "published" });
if ("ok" in plan && !plan.ok) {
return { ok: false, action: "cursor-replace", message: plan.message, changed: false };
}
if ("targetPath" in plan) {
executeCursorMcpWrite(plan);
const backupMsg = plan.backupPath ? ` Backup: ${formatUserPath(plan.backupPath) ?? plan.backupPath}.` : "";
return { ok: true, action: "cursor-replace", message: `Replaced Cursor FamiliarOS MCP config at ${formatUserPath(configPath) ?? configPath}.${backupMsg} Cursor may need to be restarted or reloaded.`, changed: true };
}
return { ok: false, action: "cursor-replace", message: "Failed to plan Cursor MCP replace.", changed: false };
} catch (error) {
return { ok: false, action: "cursor-replace", message: error instanceof Error ? error.message : "Cursor MCP replace failed.", changed: false };
}
}
export function removeCursorGlobalConfig({
homeDir,
formatUserPath,
}: {
readonly homeDir: string;
readonly formatUserPath: (path: string | undefined) => string | undefined;
}): CursorActionResult {
try {
const configPath = getCursorGlobalMcpPath(homeDir);
const plan = planCursorMcpRemove(configPath);
if ("ok" in plan && !plan.ok) {
return { ok: false, action: "cursor-remove", message: plan.message, changed: false };
}
if ("targetPath" in plan) {
executeCursorMcpWrite(plan);
return { ok: true, action: "cursor-remove", message: `Removed Cursor FamiliarOS MCP config at ${formatUserPath(configPath) ?? configPath}. Cursor may need to be restarted or reloaded.`, changed: true };
}
return { ok: false, action: "cursor-remove", message: "Failed to plan Cursor MCP remove.", changed: false };
} catch (error) {
return { ok: false, action: "cursor-remove", message: error instanceof Error ? error.message : "Cursor MCP remove failed.", changed: false };
}
}
function mapCursorStatusToState(status: CursorMcpStatusResult["status"]): CursorSetupStatus["state"] {
switch (status) {
case "installed":
return "configured";
case "missing":
return "needs_setup";
case "needs-update":
return "needs_update";
case "conflict":
return "conflict";
case "invalid":
case "error":
return "error";
default:
return "error";
}
}
function mapCursorStatusToLabel(status: CursorMcpStatusResult["status"]): string {
switch (status) {
case "installed":
return "Configured";
case "missing":
return "Not configured";
case "needs-update":
return "Needs update";
case "conflict":
return "Conflict";
case "invalid":
case "error":
return "Config error";
default:
return "Checking";
}
}
function safePrepareOpenCode(
configDir: string,
selectedPetId: string | undefined,
cliVersion: string,
pluginVersion: string,
commandMode: FamiliarOSCommandMode,
cliEntryPath: string | undefined,
): { readonly ok: true; readonly command: readonly string[]; readonly configPath: string; readonly cleanupConfigPaths: readonly string[]; readonly instructionPath: string; readonly plugin: readonly unknown[] | string; readonly configPreview: Record<string, unknown> } | { readonly ok: false; readonly message: string } {
try {
const prepared = prepareOpenCodeGlobalSetup({ configDir, petId: selectedPetId || undefined, cliVersion, pluginVersion, commandMode, cliEntryPath });
const parsed = parseOpenCodeConfig(prepared.configWrite.content);
if (!parsed.ok) return { ok: false, message: parsed.message };
const config = parsed.value as { mcp?: { familiaros?: { command?: readonly string[] } }; plugin?: readonly unknown[] };
const plugin = Array.isArray(config.plugin) ? config.plugin[config.plugin.length - 1] : undefined;
return { ok: true, command: config.mcp?.familiaros?.command ?? [], configPath: prepared.configPath, cleanupConfigPaths: prepared.cleanupConfigWrites.map((write) => write.targetPath), instructionPath: prepared.instructionPath, plugin: plugin === undefined ? [] : (plugin as readonly unknown[] | string), configPreview: parsed.value };
} catch (error) {
return { ok: false, message: error instanceof Error ? error.message : "OpenCode setup preview failed." };
}
}
export {
buildOpenCodeSetupSnapshot,
installOpenCodeGlobalConfig,
removeOpenCodeGlobalConfig,
} from "./agent-setup-editor-tools-opencode.js";
export type {
OpenCodeSetupPreview,
OpenCodeSetupStatus,
} from "./agent-setup-editor-tools-opencode.js";
export {
buildCursorSetupSnapshot,
installCursorGlobalConfig,
replaceCursorGlobalConfig,
removeCursorGlobalConfig,
} from "./agent-setup-editor-tools-cursor.js";
export type {
CursorSetupPreview,
CursorSetupStatus,
} from "./agent-setup-editor-tools-cursor.js";

View file

@ -203,6 +203,8 @@ const agentSetupActionsToolingSource = readFileSync(join(appDir, "src", "agent-s
const agentSetupCommandRunnerSource = readFileSync(join(appDir, "src", "agent-setup-command-runner.ts"), "utf8");
const agentSetupCommandContextSource = readFileSync(join(appDir, "src", "agent-setup-command-context.ts"), "utf8");
const agentSetupEditorToolsSource = readFileSync(join(appDir, "src", "agent-setup-editor-tools.ts"), "utf8");
const agentSetupEditorToolsOpenCodeSource = readFileSync(join(appDir, "src", "agent-setup-editor-tools-opencode.ts"), "utf8");
const agentSetupEditorToolsCursorSource = readFileSync(join(appDir, "src", "agent-setup-editor-tools-cursor.ts"), "utf8");
const agentSetupSupportSource = readFileSync(join(appDir, "src", "agent-setup-support.ts"), "utf8");
const pluginSdkBridgeSource = readFileSync(join(appDir, "src", "plugin-sdk-bridge.ts"), "utf8");
const pluginSdkApiBuilderSource = readFileSync(join(appDir, "src", "plugin-sdk-api-builder.ts"), "utf8");
@ -591,6 +593,12 @@ assert.match(agentSetupClaudeStatusSource, /export function formatUserPath/, "ag
assert.match(agentSetupActionsToolingSource, /export async function getOpenCodeSetup/, "agent-setup tooling seam must export OpenCode setup loading.");
assert.match(agentSetupActionsToolingSource, /export async function getCursorSetup/, "agent-setup tooling seam must export Cursor setup loading.");
assert.match(agentSetupActionsToolingSource, /export async function runAgentSetupMcpServerHealthCheck/, "agent-setup tooling seam must export MCP server health checks.");
assert.match(agentSetupEditorToolsSource, /from "\.\/agent-setup-editor-tools-opencode(?:\.js)?"/, "agent-setup editor helper barrel must compose the extracted OpenCode seam.");
assert.match(agentSetupEditorToolsSource, /from "\.\/agent-setup-editor-tools-cursor(?:\.js)?"/, "agent-setup editor helper barrel must compose the extracted Cursor seam.");
assert.match(agentSetupEditorToolsOpenCodeSource, /export function buildOpenCodeSetupSnapshot/, "agent-setup OpenCode helper seam must export setup snapshot building.");
assert.match(agentSetupEditorToolsOpenCodeSource, /export function installOpenCodeGlobalConfig/, "agent-setup OpenCode helper seam must export config writes.");
assert.match(agentSetupEditorToolsCursorSource, /export function buildCursorSetupSnapshot/, "agent-setup Cursor helper seam must export setup snapshot building.");
assert.match(agentSetupEditorToolsCursorSource, /export function replaceCursorGlobalConfig/, "agent-setup Cursor helper seam must export replace writes.");
assert.match(pluginServiceDefaultPetSource, /export async function buildDefaultPetPluginCommands/, "plugin-service default-pet helper must export default familiar command shaping.");
assert.match(pluginServiceSupportSource, /from "\.\/plugin-service-actions(?:\.js)?"/, "plugin-service support barrel must re-export the service action seam.");
assert.match(pluginServiceActionsSource, /export async function buildPluginCatalogSnapshot/, "plugin-service action seam must export catalog snapshot shaping.");

View file

@ -86,8 +86,11 @@ windows.ts (IPC handlers)
│ ├── FamiliarOS MCP preview shaping
│ ├── Claude memory install/uninstall safety wrappers
│ └── bounded action journal writes
└── agent-setup-editor-tools.ts
├── OpenCode global config management (@familiaros/opencode)
├── agent-setup-editor-tools.ts
│ └── stable barrel over the editor-specific setup helper seams
├── agent-setup-editor-tools-opencode.ts
│ └── OpenCode global config management (@familiaros/opencode)
└── agent-setup-editor-tools-cursor.ts
└── Cursor global MCP config management (@familiaros/cursor)
```
@ -216,6 +219,8 @@ plugin-service-local-support.ts → plugin-local-loader.ts validates selected fo
- `control-center-ipc-agent.ts`: Agent setup, toolkit, and FamiliarOS MCP preview/test IPC routes for the Control Center
- `agent-setup-actions.ts`: Extracted action execution shell for hook handling plus Claude MCP add/remove orchestration
- `agent-setup-actions-tooling.ts`: Extracted OpenCode/Cursor setup loading, global config actions, and FamiliarOS MCP server health checks
- `agent-setup-editor-tools-opencode.ts`: Extracted OpenCode global setup status, previews, and config write helpers
- `agent-setup-editor-tools-cursor.ts`: Extracted Cursor global setup status, previews, and MCP config write helpers
- `agent-setup-support.ts`: Extracted MCP preview, Claude memory safety wrappers, and bounded action journal helpers for agent setup
- `mcp-toolkit-installer.ts`: Public MCP toolkit installer facade for persistent host-agent server setup and managed install result shaping
- `mcp-toolkit-installer-support.ts`: Extracted MCP toolkit bundle/server-definition, argument, note, and command-candidate helpers
@ -350,7 +355,9 @@ plugin-service-local-support.ts → plugin-local-loader.ts validates selected fo
- `agent-setup-claude-status.ts`: Extracted Claude MCP preview/status detection, command wrappers, output sanitization, and packaged-resource error shaping
- `agent-setup-command-context.ts`: Agent setup command path persistence, preferred command resolution, and workspace package metadata helpers
- `agent-setup-command-runner.ts`: Extracted agent-setup command execution, PATH shaping, Claude command candidate selection, and sanitized command result summarization
- `agent-setup-editor-tools.ts`: OpenCode/Cursor global setup status, previews, and config write helpers
- `agent-setup-editor-tools.ts`: Stable barrel for the extracted OpenCode and Cursor editor setup helper seams
- `agent-setup-editor-tools-opencode.ts`: Extracted OpenCode global setup status, previews, and config write helpers
- `agent-setup-editor-tools-cursor.ts`: Extracted Cursor global setup status, previews, and MCP config write helpers
- `claude-memory.ts`: Claude instructions file management (`~/.claude/familiaros.md`)
- `update-checker.ts`: GitHub release polling, update status
- `update-version.ts`: Version parsing and comparison

View file

@ -8,6 +8,8 @@ const agentSetupSource = readFileSync(resolve(desktopRoot, "src/agent-setup.ts")
const agentSetupActionsSource = readFileSync(resolve(desktopRoot, "src/agent-setup-actions.ts"), "utf8");
const agentSetupActionsToolingSource = readFileSync(resolve(desktopRoot, "src/agent-setup-actions-tooling.ts"), "utf8");
const agentSetupEditorToolsSource = readFileSync(resolve(desktopRoot, "src/agent-setup-editor-tools.ts"), "utf8");
const agentSetupEditorToolsOpenCodeSource = readFileSync(resolve(desktopRoot, "src/agent-setup-editor-tools-opencode.ts"), "utf8");
const agentSetupEditorToolsCursorSource = readFileSync(resolve(desktopRoot, "src/agent-setup-editor-tools-cursor.ts"), "utf8");
assert.match(agentSetupSource, /from "\.\/agent-setup-actions\.js"/, "Agent setup must import the extracted action seam.");
assert.match(agentSetupActionsSource, /from "\.\/agent-setup-actions-tooling(?:\.js)?"/, "Agent setup action shell must compose the extracted tooling seam.");
@ -17,9 +19,11 @@ assert.match(agentSetupActionsToolingSource, /buildCursorSetupSnapshot/, "Agent
assert.match(agentSetupActionsToolingSource, /installOpenCodeGlobalConfig/, "Agent setup tooling seam must delegate OpenCode config writes through the extracted helper.");
assert.match(agentSetupActionsToolingSource, /replaceCursorGlobalConfig/, "Agent setup tooling seam must delegate Cursor replace writes through the extracted helper.");
assert.match(agentSetupEditorToolsSource, /export function buildOpenCodeSetupSnapshot/, "Agent setup editor helper must own the OpenCode preview/status seam.");
assert.match(agentSetupEditorToolsSource, /export function buildCursorSetupSnapshot/, "Agent setup editor helper must own the Cursor preview/status seam.");
assert.match(agentSetupEditorToolsSource, /export function installOpenCodeGlobalConfig/, "Agent setup editor helper must own OpenCode global install writes.");
assert.match(agentSetupEditorToolsSource, /export function replaceCursorGlobalConfig/, "Agent setup editor helper must own Cursor replace writes.");
assert.match(agentSetupEditorToolsSource, /from "\.\/agent-setup-editor-tools-opencode(?:\.js)?"/, "Agent setup editor helper barrel must compose the extracted OpenCode seam.");
assert.match(agentSetupEditorToolsSource, /from "\.\/agent-setup-editor-tools-cursor(?:\.js)?"/, "Agent setup editor helper barrel must compose the extracted Cursor seam.");
assert.match(agentSetupEditorToolsOpenCodeSource, /export function buildOpenCodeSetupSnapshot/, "Agent setup OpenCode helper must own the OpenCode preview/status seam.");
assert.match(agentSetupEditorToolsOpenCodeSource, /export function installOpenCodeGlobalConfig/, "Agent setup OpenCode helper must own OpenCode global install writes.");
assert.match(agentSetupEditorToolsCursorSource, /export function buildCursorSetupSnapshot/, "Agent setup Cursor helper must own the Cursor preview/status seam.");
assert.match(agentSetupEditorToolsCursorSource, /export function replaceCursorGlobalConfig/, "Agent setup Cursor helper must own Cursor replace writes.");
console.error("Agent setup editor helper seam validation passed.");