Spaces:
Build error
Build error
| 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<string, ClientContextProfile>; | |
| } | |
| 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<string, unknown>; | |
| 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<string, unknown>; | |
| const version = record.version === 1 ? 1 : 1; | |
| const currentProfile = toStringOrUndefined(record.currentProfile) ?? DEFAULT_PROFILE; | |
| const rawProfiles = record.profiles; | |
| const profiles: Record<string, ClientContextProfile> = {}; | |
| if (typeof rawProfiles === "object" && rawProfiles !== null && !Array.isArray(rawProfiles)) { | |
| for (const [name, profile] of Object.entries(rawProfiles as Record<string, unknown>)) { | |
| 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<ClientContextProfile>, | |
| 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 }; | |
| } | |