import { extname, isAbsolute, relative, resolve } from "path"; import type { CliCommandContext } from "../../types/plugin"; import type { MarketContext } from "../types"; import { withMarketData } from "../context"; import { cliStyles, renderStats, type CliStatEntry } from "../../utils/cli-output"; import { filterPaneCatalogEntries, renderPaneCatalogReport, type PaneFunctionCatalog, } from "./catalog"; import { createPaneCatalog } from "./discovery"; import { normalizeLookupToken, optionPaneState, parseArgumentsOption, parsePaneCatalogArgs, parsePaneFunctionArgs, type ParsedPaneFunctionArgs, } from "./options"; import { resolvePaneFunction, type ResolvedPaneFunction } from "./resolver"; import { buildFunctionReport } from "./report"; import { defaultScreenshotPath, renderDesktopShot } from "./screenshot"; import { buildPaneCatalogEntries, } from "./catalog"; import { capabilityPluginState, getPaneFunctionCapability, isDataPaneForDomFallback, normalizeCapabilityOptions, } from "./capabilities"; import { withPersistedCloudSession } from "./cloud-session"; async function withPaneRuntime( ctx: CliCommandContext, args: string[], run: (runtime: { parsed: ParsedPaneFunctionArgs; context: MarketContext; registry: PaneFunctionCatalog; resolved: ResolvedPaneFunction; }) => Promise, settings: { strictHeadlessOptions?: boolean } = {}, ): Promise { const parsed = parsePaneFunctionArgs(args, ctx.cliOptions); return withMarketData(ctx, async (market) => { const context: MarketContext = ctx.cliOptions.refresh ? { ...market, refresh: true } : market; const registry = await createPaneCatalog(context, ctx.plugins); try { const resolved = await resolvePaneFunction(registry, context, parsed, settings); return await run({ parsed, context, registry, resolved }); } finally { registry.destroy(); } }); } export async function runPaneFunction(args: string[], ctx: CliCommandContext) { await runPaneCliCommand(ctx, async () => { await withPaneRuntime(ctx, args, async ({ parsed, context, resolved }) => { if (parsed.requireBotSafe && ( !resolved.capability.botSafe || resolved.capability.reportReadiness !== "ready" )) { throw new Error( `${resolved.token} is not a verified bot-safe report capability. ` + `Use "gloomberb catalog ${resolved.token}" to inspect readiness.`, ); } const report = await withPersistedCloudSession( context, () => buildFunctionReport(resolved, context, parsed.arg), ); if (parsed.requireBotSafe && (report.data.empty || !report.data.complete)) { const unavailable = report.data.unavailableSymbols.length > 0 ? ` Missing data for ${report.data.unavailableSymbols.join(", ")}.` : ""; throw new Error( `${resolved.token} did not produce a complete bot-safe report.${unavailable}`, ); } ctx.printResult({ data: report.data }, { text: () => report.text, }); }, { strictHeadlessOptions: true }); }); } export async function runPaneScreenshot(args: string[], ctx: CliCommandContext) { await runPaneCliCommand(ctx, async () => { await withPaneRuntime(ctx, args, async ({ parsed, context, resolved }) => { if (parsed.requireBotSafe && ( !resolved.capability.botSafe || resolved.capability.screenshotReadiness !== "ready" )) { throw new Error( `${resolved.token} is not a verified bot-safe screenshot capability. ` + `Use "gloomberb catalog ${resolved.token}" to inspect readiness.`, ); } const outputPath = parsed.outputPath ? resolve(process.cwd(), ensurePngExtension(parsed.outputPath)) : defaultScreenshotPath(resolved, parsed.arg); const result = await renderDesktopShot({ resolved, context, rawArg: parsed.arg, outputPath, width: parsed.width, height: parsed.height, theme: parsed.theme, scale: parsed.scale, watermark: parsed.watermark, options: parsed.options, }); if (parsed.requireBotSafe && !result.usable) { throw new Error( `${resolved.token} did not produce a usable bot-safe screenshot: ${result.unusableReason ?? "unknown reason"}`, ); } ctx.printResult({ data: result }, { text: (data) => { const issues = [ data.empty ? "empty" : null, data.complete ? null : "incomplete", data.semanticMismatch ? "does not match the data" : null, data.usable ? null : "not usable", ].filter((issue): issue is string => issue != null); const stats: CliStatEntry[] = [ ["Rows", String(data.rowCount)], ["Status", issues.length > 0 ? cliStyles.warning(issues.join(", ")) : cliStyles.success("complete")], ]; if (data.unusableReason) stats.push(["Reason", data.unusableReason]); if (data.unavailableSymbols.length > 0) stats.push(["No data for", data.unavailableSymbols.join(", ")]); if (data.render.emptyStateMarkers.length > 0) stats.push(["Empty states", data.render.emptyStateMarkers.join(", ")]); if (data.render.missingExpectedText.length > 0) stats.push(["Missing text", data.render.missingExpectedText.join(", ")]); return [`Saved ${displayPath(data.outputPath)}`, renderStats(stats)].join("\n"); }, }); }); }); } export async function runPaneCatalog(args: string[], ctx: CliCommandContext) { await runPaneCliCommand(ctx, async () => { const parsed = parsePaneCatalogArgs(args); const effectiveParsed = { ...parsed, limit: ctx.cliOptions.limit ?? parsed.limit, }; await withMarketData(ctx, async (context) => { const registry = await createPaneCatalog(context, ctx.plugins); try { const entries = await buildPaneCatalogEntries(registry, context); const botSafeEntries = effectiveParsed.botSafeOnly ? entries.filter((entry) => entry.capability.botSafe) : entries; const filtered = filterPaneCatalogEntries(botSafeEntries, parsed.query); ctx.printResult({ data: filtered.slice(0, effectiveParsed.limit) }, { text: () => renderPaneCatalogReport(filtered, effectiveParsed), }); } finally { registry.destroy(); } }); }); } async function runPaneCliCommand(ctx: CliCommandContext, run: () => Promise) { try { await run(); } catch (error) { const message = error instanceof Error ? error.message : String(error); ctx.fail(message); } } /** A path under the working directory reads shorter relative to it. */ function displayPath(path: string): string { const relativePath = relative(process.cwd(), path); return relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath) ? relativePath : path; } function ensurePngExtension(path: string): string { return extname(path) ? path : `${path}.png`; } export const paneFunctionTestInternals = { parsePaneFunctionArgs, parsePaneCatalogArgs, normalizeLookupToken, parseArgumentsOption, optionPaneState, filterPaneCatalogEntries, renderPaneCatalogReport, getPaneFunctionCapability, isDataPaneForDomFallback, normalizeCapabilityOptions, capabilityPluginState, };