import fs from "node:fs"; import path from "node:path"; import { resolveDefaultContextPath } from "../config/home.js"; const DEFAULT_CONTEXT_BASENAME = "context.json"; const DEFAULT_PROFILE = "default"; export interface ClientContextProfile { apiBase?: string; companyId?: string; apiKeyEnvVarName?: string; } export interface ClientContext { version: 1; currentProfile: string; profiles: Record; } function findContextFileFromAncestors(startDir: string): string | null { const absoluteStartDir = path.resolve(startDir); let currentDir = absoluteStartDir; while (true) { const candidate = path.resolve(currentDir, ".paperclip", DEFAULT_CONTEXT_BASENAME); if (fs.existsSync(candidate)) { return candidate; } const nextDir = path.resolve(currentDir, ".."); if (nextDir === currentDir) break; currentDir = nextDir; } return null; } export function resolveContextPath(overridePath?: string): string { if (overridePath) return path.resolve(overridePath); if (process.env.PAPERCLIP_CONTEXT) return path.resolve(process.env.PAPERCLIP_CONTEXT); return findContextFileFromAncestors(process.cwd()) ?? resolveDefaultContextPath(); } export function defaultClientContext(): ClientContext { return { version: 1, currentProfile: DEFAULT_PROFILE, profiles: { [DEFAULT_PROFILE]: {}, }, }; } function parseJson(filePath: string): unknown { try { return JSON.parse(fs.readFileSync(filePath, "utf-8")); } catch (err) { throw new Error(`Failed to parse JSON at ${filePath}: ${err instanceof Error ? err.message : String(err)}`); } } function toStringOrUndefined(value: unknown): string | undefined { return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; } function normalizeProfile(value: unknown): ClientContextProfile { if (typeof value !== "object" || value === null || Array.isArray(value)) return {}; const profile = value as Record; return { apiBase: toStringOrUndefined(profile.apiBase), companyId: toStringOrUndefined(profile.companyId), apiKeyEnvVarName: toStringOrUndefined(profile.apiKeyEnvVarName), }; } function normalizeContext(raw: unknown): ClientContext { if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { return defaultClientContext(); } const record = raw as Record; const version = record.version === 1 ? 1 : 1; const currentProfile = toStringOrUndefined(record.currentProfile) ?? DEFAULT_PROFILE; const rawProfiles = record.profiles; const profiles: Record = {}; if (typeof rawProfiles === "object" && rawProfiles !== null && !Array.isArray(rawProfiles)) { for (const [name, profile] of Object.entries(rawProfiles as Record)) { if (!name.trim()) continue; profiles[name] = normalizeProfile(profile); } } if (!profiles[currentProfile]) { profiles[currentProfile] = {}; } if (Object.keys(profiles).length === 0) { profiles[DEFAULT_PROFILE] = {}; } return { version, currentProfile, profiles, }; } export function readContext(contextPath?: string): ClientContext { const filePath = resolveContextPath(contextPath); if (!fs.existsSync(filePath)) { return defaultClientContext(); } const raw = parseJson(filePath); return normalizeContext(raw); } export function writeContext(context: ClientContext, contextPath?: string): void { const filePath = resolveContextPath(contextPath); const dir = path.dirname(filePath); fs.mkdirSync(dir, { recursive: true }); const normalized = normalizeContext(context); fs.writeFileSync(filePath, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 }); } export function upsertProfile( profileName: string, patch: Partial, contextPath?: string, ): ClientContext { const context = readContext(contextPath); const existing = context.profiles[profileName] ?? {}; const merged: ClientContextProfile = { ...existing, ...patch, }; if (patch.apiBase !== undefined && patch.apiBase.trim().length === 0) { delete merged.apiBase; } if (patch.companyId !== undefined && patch.companyId.trim().length === 0) { delete merged.companyId; } if (patch.apiKeyEnvVarName !== undefined && patch.apiKeyEnvVarName.trim().length === 0) { delete merged.apiKeyEnvVarName; } context.profiles[profileName] = merged; context.currentProfile = context.currentProfile || profileName; writeContext(context, contextPath); return context; } export function setCurrentProfile(profileName: string, contextPath?: string): ClientContext { const context = readContext(contextPath); if (!context.profiles[profileName]) { context.profiles[profileName] = {}; } context.currentProfile = profileName; writeContext(context, contextPath); return context; } export function resolveProfile( context: ClientContext, profileName?: string, ): { name: string; profile: ClientContextProfile } { const name = profileName?.trim() || context.currentProfile || DEFAULT_PROFILE; const profile = context.profiles[name] ?? {}; return { name, profile }; }