gloomberb 0.17.2 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +18 -8
- package/src/api-client/types.ts +3 -1
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +18 -1
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +32 -4
- package/src/plugins/builtin/futures/model.ts +65 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -9
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/styles.css +6 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +19 -2
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DesktopPaneShotRenderedRow } from "../desktop-pane-shot";
|
|
2
|
-
import { cliStyles, renderSection, renderTable } from "../../utils/cli-output";
|
|
2
|
+
import { cliStyles, cliTerminalWidth, renderSection, renderTable, wrapText } from "../../utils/cli-output";
|
|
3
3
|
import { formatUtcTime } from "../../utils/utc-time";
|
|
4
4
|
import type { MarketContext } from "../types";
|
|
5
5
|
import type { PaneFunctionReport } from "./report";
|
|
@@ -14,10 +14,14 @@ import { renderedReportFailure, renderedReportFreshness, renderedReportNotices,
|
|
|
14
14
|
|
|
15
15
|
const DOM_REPORT_WIDTH = 1280;
|
|
16
16
|
const DOM_REPORT_HEIGHT = 720;
|
|
17
|
+
/** A pane whose reader has a page open grows to show all of it, up to about 18,000 px: a report saves no image to bound it. */
|
|
18
|
+
const DOM_REPORT_MAX_HEIGHT_CELLS = 1000;
|
|
17
19
|
const DOM_LIMITATION =
|
|
18
20
|
"Only values visible in the rendered pane are returned; clipped or off-screen data may be omitted.";
|
|
19
21
|
const DOM_EVERY_ROW =
|
|
20
22
|
"Every row of the pane's table is returned, including rows scrolled out of view.";
|
|
23
|
+
const DOM_WHOLE_PANE =
|
|
24
|
+
"The pane was grown to fit the page it has open, so all of its text is returned.";
|
|
21
25
|
|
|
22
26
|
/** The sign-in wall's buttons, which a rendered view reads as words: `Log in ↵ Sign up free`. */
|
|
23
27
|
const SIGN_IN_WALL_BUTTONS = /\s*\b(?:Log in|Sign up free|Resend Verification Email)(?:\s*\u21B5)?(?=\s|$)/g;
|
|
@@ -39,6 +43,28 @@ function signInWallText(visibleText: string): string {
|
|
|
39
43
|
return headline ? `${headline} ${needs}` : needs;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
const LIST_MARKER = /^\s*[\u2022\u25E6\u25AA\u00B7\u2013-]\s*$/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The words of a pane that is prose, not a table, a line to each block it draws
|
|
50
|
+
* and wrapped to the terminal's width (100 where it has none): the pane's own title,
|
|
51
|
+
* which the report's heading says already, is left off its top.
|
|
52
|
+
*/
|
|
53
|
+
function proseLines(render: PaneScreenshotResult["render"], title: string): string[] {
|
|
54
|
+
const lines = render.visibleLines?.length ? render.visibleLines : [render.visibleText];
|
|
55
|
+
const width = Math.min(cliTerminalWidth() ?? 100, 100);
|
|
56
|
+
const titled = lines[0]?.trim() === title ? lines.slice(1) : lines;
|
|
57
|
+
const body: string[] = [];
|
|
58
|
+
// A stack's detail opens under its Back button, a control and not part of the page.
|
|
59
|
+
for (const line of titled[0] === "Back" ? titled.slice(1) : titled) {
|
|
60
|
+
// A list marker is drawn as a block of its own, ahead of the item's words.
|
|
61
|
+
const marker = body.at(-1);
|
|
62
|
+
if (marker !== undefined && LIST_MARKER.test(marker)) body[body.length - 1] = `${marker.trim()} ${line}`;
|
|
63
|
+
else body.push(line);
|
|
64
|
+
}
|
|
65
|
+
return body.flatMap((line) => wrapText(line, width));
|
|
66
|
+
}
|
|
67
|
+
|
|
42
68
|
function rowsContainEllipsis(rows: DesktopPaneShotRenderedRow[]): boolean {
|
|
43
69
|
return rows.some((row) => row.cells.some((cell) => /\u2026|\.\.\./.test(cell.text)));
|
|
44
70
|
}
|
|
@@ -215,7 +241,7 @@ export function buildDomPaneReportFromRender(
|
|
|
215
241
|
} else if (lockNote) {
|
|
216
242
|
textLines.push(...lockNoteLines(lockNote));
|
|
217
243
|
} else if (result.render.visibleText) {
|
|
218
|
-
textLines.push(gate === "sign-in" ? signInWallText(result.render.visibleText) : result.render.
|
|
244
|
+
textLines.push(...(gate === "sign-in" ? [signInWallText(result.render.visibleText)] : proseLines(result.render, resolved.label)));
|
|
219
245
|
} else {
|
|
220
246
|
textLines.push(failureReason ?? "No rendered values were available.");
|
|
221
247
|
}
|
|
@@ -231,6 +257,8 @@ export function buildDomPaneReportFromRender(
|
|
|
231
257
|
const complete = failureReason === null && !truncated;
|
|
232
258
|
// Each table published its rows and nothing else was cut: the report is the whole view.
|
|
233
259
|
const everyRow = complete && result.render.reportRows !== undefined;
|
|
260
|
+
// A reader grown to fit its page (a docs page) and not cut once grown is read whole.
|
|
261
|
+
const wholePane = complete && !everyRow && resolved.pane.shotFitsReader === true;
|
|
234
262
|
return {
|
|
235
263
|
data: {
|
|
236
264
|
kind: "rendered-view",
|
|
@@ -247,7 +275,8 @@ export function buildDomPaneReportFromRender(
|
|
|
247
275
|
truncated,
|
|
248
276
|
truncationReasons,
|
|
249
277
|
...(everyRow ? { everyRow } : {}),
|
|
250
|
-
|
|
278
|
+
...(wholePane ? { wholePane } : {}),
|
|
279
|
+
limitation: everyRow ? DOM_EVERY_ROW : wholePane ? DOM_WHOLE_PANE : DOM_LIMITATION,
|
|
251
280
|
...(lockReason ?? failureReason ? { reason: lockReason ?? failureReason } : {}),
|
|
252
281
|
...(notices.length > 0 ? { metadata: { ...rendered.facts, notices } } : {}),
|
|
253
282
|
...(locked ? { locked } : { freshness }),
|
|
@@ -278,12 +307,12 @@ export function appendDomReportFooter(
|
|
|
278
307
|
].join("\n");
|
|
279
308
|
}
|
|
280
309
|
|
|
281
|
-
/** How much of the pane a rendered-view report holds: none behind a wall, cut, every row its tables publish, or what the viewport shows. */
|
|
282
|
-
export type DomReportView = "locked" | "clipped" | "every-row" | "visible";
|
|
310
|
+
/** How much of the pane a rendered-view report holds: none behind a wall, cut, every row its tables publish, all of a pane grown to fit, or what the viewport shows. */
|
|
311
|
+
export type DomReportView = "locked" | "clipped" | "every-row" | "whole" | "visible";
|
|
283
312
|
|
|
284
313
|
export function domReportView(data: Readonly<Record<string, unknown>>): DomReportView {
|
|
285
314
|
if (data.locked) return "locked";
|
|
286
|
-
return data.truncated === true ? "clipped" : data.everyRow === true ? "every-row" : "visible";
|
|
315
|
+
return data.truncated === true ? "clipped" : data.everyRow === true ? "every-row" : data.wholePane === true ? "whole" : "visible";
|
|
287
316
|
}
|
|
288
317
|
|
|
289
318
|
/** What a rendered-view report says about where its values come from. */
|
|
@@ -292,6 +321,8 @@ export function domReportNote(elapsedMs: number, view: DomReportView): string {
|
|
|
292
321
|
? "Rendered view: the pane is locked, so no values were read."
|
|
293
322
|
: view === "every-row"
|
|
294
323
|
? "Rendered view: every row of the pane's table, including rows scrolled out of view."
|
|
324
|
+
: view === "whole"
|
|
325
|
+
? "Rendered view: the pane grown to fit the whole page."
|
|
295
326
|
: `Rendered view: values come from the visible pane. ${view === "clipped" ? "The rendered view is clipped." : "The rendered view may be clipped."}`;
|
|
296
327
|
return `${source} Render time: ${elapsedMs} ms.`;
|
|
297
328
|
}
|
|
@@ -356,6 +387,8 @@ export async function buildDomFunctionReport(
|
|
|
356
387
|
theme: null,
|
|
357
388
|
scale: 1,
|
|
358
389
|
watermark: null,
|
|
390
|
+
fitReader: true,
|
|
391
|
+
maxHeightCells: DOM_REPORT_MAX_HEIGHT_CELLS,
|
|
359
392
|
options: {},
|
|
360
393
|
captureImage: false,
|
|
361
394
|
});
|
|
@@ -10,8 +10,8 @@ import type {
|
|
|
10
10
|
} from "../../types/plugin";
|
|
11
11
|
import { localTimeSuffix, parseReportTime, type ReportTime } from "../../utils/utc-time";
|
|
12
12
|
import { isRecord } from "../../utils/guards";
|
|
13
|
-
import { latestRegularSessionClose } from "../../market-data/market/freshness";
|
|
14
|
-
import { reportMarketSession, type ReportMarket, type SessionObservation } from "./market-session";
|
|
13
|
+
import { latestRegularSessionClose, readIndexSession } from "../../market-data/market/freshness";
|
|
14
|
+
import { reportMarketSession, reportVenueSessionDates, type ReportMarket, type SessionObservation, type VenueSessionDate } from "./market-session";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The four states of docs/usage.md#how-current-a-report-is, plus `unreported`
|
|
@@ -53,6 +53,12 @@ export interface ReportFreshness {
|
|
|
53
53
|
tradingDayMarket?: string;
|
|
54
54
|
/** Where the venues of a quote report stand now, from their session calendars. */
|
|
55
55
|
market?: ReportMarket;
|
|
56
|
+
/**
|
|
57
|
+
* With a declared `datesEachVenue`, when the rows trade on more than one
|
|
58
|
+
* market: each market's session date, which the status line gives in place
|
|
59
|
+
* of the newest row's alone.
|
|
60
|
+
*/
|
|
61
|
+
venueSessions?: VenueSessionDate[];
|
|
56
62
|
/**
|
|
57
63
|
* For a dataset (declared `dataset`, with no quote among its rows): whole UTC
|
|
58
64
|
* days from the last day its newest observation covers to when this report
|
|
@@ -76,6 +82,8 @@ export interface ReportFreshness {
|
|
|
76
82
|
oldestAgeMinutes?: number;
|
|
77
83
|
/** What the figures are, said after the status: `funding = current estimate, pays 08:00 UTC`. */
|
|
78
84
|
note?: string;
|
|
85
|
+
/** When the figures reach readers and when the next are due, said right after the as-of date. */
|
|
86
|
+
schedule?: string;
|
|
79
87
|
/** Who published the figures, when the data names them. */
|
|
80
88
|
publisher?: string;
|
|
81
89
|
/** When the publisher released the data, an ISO instant in UTC or an ISO date, when the data states it. */
|
|
@@ -145,10 +153,12 @@ function readFeedSignals(record: Record<string, unknown>, signals: FeedSignals):
|
|
|
145
153
|
function readSession(record: Record<string, unknown>, time: ReportTime | null, signals: FeedSignals): void {
|
|
146
154
|
const exchange = record.sessionExchange;
|
|
147
155
|
if (typeof exchange !== "string" || !exchange.trim()) return;
|
|
156
|
+
const calendar = readIndexSession(record.sessionCalendar);
|
|
148
157
|
signals.sessions.push({
|
|
149
158
|
exchange,
|
|
150
159
|
...(typeof record.marketState === "string" ? { marketState: record.marketState } : {}),
|
|
151
160
|
time,
|
|
161
|
+
...(calendar ? { calendar } : {}),
|
|
152
162
|
});
|
|
153
163
|
}
|
|
154
164
|
|
|
@@ -404,6 +414,7 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
|
|
|
404
414
|
}
|
|
405
415
|
}
|
|
406
416
|
const { market, closeDate, closeMarket } = reportMarketSession(signals.sessions, now);
|
|
417
|
+
const venueSessions = declared.datesEachVenue && !declaredAsOf ? reportVenueSessionDates(signals.sessions, now) : [];
|
|
407
418
|
// The close wording dates the newest observation, so only when that is what `asOf` is.
|
|
408
419
|
const asOfClose = closeDate && asOf && !declaredAsOf ? closeDate : null;
|
|
409
420
|
const tradingDayMarket = asOfClose ? closeMarket
|
|
@@ -417,6 +428,7 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
|
|
|
417
428
|
const observation = lagDays != null ? declared.observation?.trim() : undefined;
|
|
418
429
|
|
|
419
430
|
const note = declared.note?.trim();
|
|
431
|
+
const schedule = declared.schedule?.trim();
|
|
420
432
|
const publisher = declared.publisher?.trim();
|
|
421
433
|
const publishedAt = parseReportTime(declared.publishedAt);
|
|
422
434
|
return {
|
|
@@ -434,11 +446,13 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
|
|
|
434
446
|
...(asOfClose ? { asOfClose } : {}),
|
|
435
447
|
...(tradingDayMarket ? { tradingDayMarket } : {}),
|
|
436
448
|
...(market ? { market } : {}),
|
|
449
|
+
...(venueSessions.length > 0 ? { venueSessions } : {}),
|
|
437
450
|
...(lagDays != null ? { lagDays } : {}),
|
|
438
451
|
...(observation ? { observation } : {}),
|
|
439
452
|
...(sources.length ? { sources: sources.map(({ source, time }) => ({ source, asOf: isoTime(time) })) } : {}),
|
|
440
453
|
...(oldestAgeMinutes != null ? { oldestAgeMinutes } : {}),
|
|
441
454
|
...(note ? { note } : {}),
|
|
455
|
+
...(schedule ? { schedule } : {}),
|
|
442
456
|
...(publisher ? { publisher } : {}),
|
|
443
457
|
...(publishedAt ? { publishedAt: isoTime(publishedAt) } : {}),
|
|
444
458
|
retrievedAt: new Date(now).toISOString(),
|
|
@@ -628,6 +642,26 @@ function onRetrievalDay(iso: string, retrievedAt: string): boolean {
|
|
|
628
642
|
return iso.slice(0, 10) === retrievedAt.slice(0, 10);
|
|
629
643
|
}
|
|
630
644
|
|
|
645
|
+
/** `US and XETRA`, `US, XETRA and EPA`. */
|
|
646
|
+
function listText(items: readonly string[]): string {
|
|
647
|
+
return items.length <= 1 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Each market's session date, markets on the same day and state said once:
|
|
652
|
+
* `US, XETRA and EPA Tue 13 Oct 2026 close`, or `US Tue 13 Oct 2026 close,
|
|
653
|
+
* XETRA Wed 14 Oct 2026 session` while Xetra trades.
|
|
654
|
+
*/
|
|
655
|
+
function venueSessionsText(sessions: readonly VenueSessionDate[]): string {
|
|
656
|
+
const groups: Array<{ markets: string[]; date: string; close: boolean }> = [];
|
|
657
|
+
for (const { market, date, close } of sessions) {
|
|
658
|
+
const group = groups.find((entry) => entry.date === date && entry.close === close);
|
|
659
|
+
if (group) group.markets.push(market);
|
|
660
|
+
else groups.push({ markets: [market], date, close });
|
|
661
|
+
}
|
|
662
|
+
return groups.map(({ markets, date, close }) => `${listText(markets)} ${dayText(date)} ${close ? "close" : "session"}`).join(", ");
|
|
663
|
+
}
|
|
664
|
+
|
|
631
665
|
/**
|
|
632
666
|
* The dated as-of: a session close, a UTC time, a date, or when the report was
|
|
633
667
|
* retrieved; with the oldest observation unless `short`, or when the line says
|
|
@@ -636,6 +670,7 @@ function onRetrievalDay(iso: string, retrievedAt: string): boolean {
|
|
|
636
670
|
*/
|
|
637
671
|
function asOfText(freshness: ReportFreshness, short: boolean, local: boolean, undated = false): string {
|
|
638
672
|
if (!freshness.asOf) return `retrieved ${timeText(freshness.retrievedAt, local)}`;
|
|
673
|
+
if (freshness.venueSessions?.length) return venueSessionsText(freshness.venueSessions);
|
|
639
674
|
const market = freshness.tradingDayMarket;
|
|
640
675
|
const period = freshness.periodicity;
|
|
641
676
|
const time = parseReportTime(freshness.asOf);
|
|
@@ -713,6 +748,8 @@ interface StatusLineDetail {
|
|
|
713
748
|
date?: "none";
|
|
714
749
|
/** Leave out what the figures are (`funding = current estimate`). */
|
|
715
750
|
note?: "none";
|
|
751
|
+
/** Leave out when the figures are published and the next are due. */
|
|
752
|
+
schedule?: "none";
|
|
716
753
|
}
|
|
717
754
|
|
|
718
755
|
/**
|
|
@@ -728,6 +765,7 @@ function statusLineParts(freshness: ReportFreshness, detail: StatusLineDetail =
|
|
|
728
765
|
return [
|
|
729
766
|
...(freshness.sources?.length ? sourcesAsOfParts(freshness, freshness.sources, undated)
|
|
730
767
|
: [asOfText(freshness, detail.asOf === "short", local, undated)]),
|
|
768
|
+
...(freshness.schedule && detail.schedule !== "none" ? [freshness.schedule] : []),
|
|
731
769
|
...(freshness.oldestAgeMinutes != null ? [`oldest ${ageText(freshness.oldestAgeMinutes)}`] : []),
|
|
732
770
|
statusText(freshness, detail.status === "short"),
|
|
733
771
|
...(freshness.market && detail.market !== "none" ? [marketText(freshness.market, now, detail.market === "short", local)] : []),
|
|
@@ -812,13 +850,13 @@ export function paneStatusLine(freshness: ReportFreshness): string {
|
|
|
812
850
|
/**
|
|
813
851
|
* `paneStatusLine` from longest to shortest, for a footer to show the first
|
|
814
852
|
* that fits beside its keys: the market part shortens, the stale total goes,
|
|
815
|
-
* then what the figures are.
|
|
853
|
+
* then what the figures are, then when they are published.
|
|
816
854
|
*/
|
|
817
855
|
export function paneStatusLineVariants(freshness: ReportFreshness): string[] {
|
|
818
856
|
// A dataset's line has no market or local time to leave out: it reads as its report and capture do.
|
|
819
857
|
if (datesObservation(freshness)) return statusLineVariants(freshness);
|
|
820
858
|
const pane: StatusLineDetail = { local: "none", date: "none" };
|
|
821
|
-
const details: StatusLineDetail[] = [pane, { ...pane, market: "short" }, { ...pane, market: "short", status: "short" }, { ...pane, market: "short", status: "short", note: "none" }];
|
|
859
|
+
const details: StatusLineDetail[] = [pane, { ...pane, market: "short" }, { ...pane, market: "short", status: "short" }, { ...pane, market: "short", status: "short", note: "none" }, { ...pane, market: "short", status: "short", note: "none", schedule: "none" }];
|
|
822
860
|
return [...new Set(details.map((detail) => capitalize(statusLineParts(freshness, detail).join(" · "))))];
|
|
823
861
|
}
|
|
824
862
|
|
|
@@ -855,9 +893,14 @@ export function statusLineVariants(freshness: ReportFreshness, { source = false
|
|
|
855
893
|
// A line that dates each source keeps every source's time and their age to
|
|
856
894
|
// the last: its day goes first, then what the figures are.
|
|
857
895
|
const shortest = details.at(-1)!;
|
|
896
|
+
// What the figures are goes before when they are published.
|
|
897
|
+
const trailing: StatusLineDetail[] = [
|
|
898
|
+
...(freshness.note ? [{ ...shortest, note: "none" as const }] : []),
|
|
899
|
+
...(freshness.schedule ? [{ ...shortest, note: "none" as const, schedule: "none" as const }] : []),
|
|
900
|
+
];
|
|
858
901
|
const variants: StatusLineDetail[] = freshness.sources?.length
|
|
859
902
|
? [{}, ...details.map((detail) => ({ ...detail, date: "none" as const })), { ...shortest, date: "none", note: "none" }]
|
|
860
|
-
:
|
|
903
|
+
: [...details, ...trailing];
|
|
861
904
|
const lines = [...new Set(variants.map((detail) => capitalize(statusLineParts(freshness, detail).join(" · "))))];
|
|
862
905
|
// A line that dates each source names them already.
|
|
863
906
|
if (!source || freshness.sources?.length) return lines;
|
|
@@ -749,7 +749,7 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
749
749
|
bloomberg: [],
|
|
750
750
|
},
|
|
751
751
|
FUT: {
|
|
752
|
-
summary: "
|
|
752
|
+
summary: "Each root's most active futures contract across equity index, rates, energy, metals, agriculture, livestock and FX with last price, unit and session change, grouped by sector.",
|
|
753
753
|
usage: ["FUT"],
|
|
754
754
|
keys: [SEARCH, OPEN],
|
|
755
755
|
data: same(DELAYED),
|
|
@@ -5,6 +5,7 @@ import { findNamedCollection } from "../../portfolio/collection-name";
|
|
|
5
5
|
import { apiClient } from "../../api-client";
|
|
6
6
|
import type { MarketContext } from "../types";
|
|
7
7
|
import type {
|
|
8
|
+
HeadlessArgumentExpansion,
|
|
8
9
|
HeadlessBundleResult,
|
|
9
10
|
HeadlessBundleSection,
|
|
10
11
|
HeadlessPaneColumn,
|
|
@@ -37,6 +38,7 @@ import type { ResolvedSeries } from "../../time-series/types";
|
|
|
37
38
|
import type { PaneFunctionReport } from "./report";
|
|
38
39
|
import type { ResolvedPaneFunction } from "./resolver";
|
|
39
40
|
import { isRecord } from "../../utils/guards";
|
|
41
|
+
import { splitSymbolArgs } from "../symbol-args";
|
|
40
42
|
import { formatUtcTime, localTimeSuffix } from "../../utils/utc-time";
|
|
41
43
|
import { deriveHeadlessFreshness, formatFreshnessLine, staleRowsText, type ReportFreshness } from "./freshness";
|
|
42
44
|
import { accessGateStatus, errorsAccessGate, isProLock } from "./access-gate";
|
|
@@ -74,7 +76,7 @@ function normalizeSymbol(value: string): string {
|
|
|
74
76
|
function normalizeSymbolList(value: string): string[] {
|
|
75
77
|
const symbols: string[] = [];
|
|
76
78
|
const seen = new Set<string>();
|
|
77
|
-
for (const part of
|
|
79
|
+
for (const part of splitSymbolArgs([value])) {
|
|
78
80
|
const symbol = normalizeSymbol(part);
|
|
79
81
|
if (!symbol || seen.has(symbol)) continue;
|
|
80
82
|
seen.add(symbol);
|
|
@@ -162,22 +164,17 @@ async function loadHeadlessPaneModel(
|
|
|
162
164
|
return result;
|
|
163
165
|
}
|
|
164
166
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
type HeadlessMarketContext = Pick<MarketContext, "config" | "store" | "refresh"> & Partial<Pick<MarketContext, "persistence" | "seriesCapabilities">> & {
|
|
168
|
+
dataProvider: HeadlessPaneContext["marketData"];
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/** What a headless loader reads the market, the account and the saved collections through. */
|
|
172
|
+
function createHeadlessPaneContext(
|
|
170
173
|
resolved: ResolvedPaneFunction,
|
|
171
|
-
context:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
signal: AbortSignal = new AbortController().signal,
|
|
176
|
-
): Promise<LoadedHeadlessPaneModel> {
|
|
177
|
-
const definition = resolved.headless;
|
|
178
|
-
if (!definition) throw new Error(`${resolved.token} has no headless pane model.`);
|
|
179
|
-
const args = buildHeadlessPaneLoadArgs(definition, resolved.token, rawArgument, resolved.options);
|
|
180
|
-
const headlessContext: HeadlessPaneContext = {
|
|
174
|
+
context: HeadlessMarketContext,
|
|
175
|
+
signal: AbortSignal,
|
|
176
|
+
): HeadlessPaneContext {
|
|
177
|
+
return {
|
|
181
178
|
marketData: context.dataProvider,
|
|
182
179
|
apiClient,
|
|
183
180
|
config: context.config,
|
|
@@ -203,7 +200,40 @@ export async function loadResolvedHeadlessPaneModel(
|
|
|
203
200
|
return { symbol: parsed.symbol, exchange: parsed.exchange ?? ticker?.metadata.exchange };
|
|
204
201
|
},
|
|
205
202
|
};
|
|
206
|
-
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The argument a function widens before it runs (`expandArgument`, RV's
|
|
207
|
+
* `--peers`), with the settings that say how; null when it runs as typed.
|
|
208
|
+
*/
|
|
209
|
+
export async function expandResolvedArgument(
|
|
210
|
+
resolved: ResolvedPaneFunction,
|
|
211
|
+
context: HeadlessMarketContext,
|
|
212
|
+
rawArgument: string,
|
|
213
|
+
signal: AbortSignal = new AbortController().signal,
|
|
214
|
+
): Promise<HeadlessArgumentExpansion | null> {
|
|
215
|
+
const definition = resolved.headless;
|
|
216
|
+
if (!definition?.expandArgument) return null;
|
|
217
|
+
const args = buildHeadlessPaneLoadArgs(definition, resolved.token, rawArgument, resolved.options);
|
|
218
|
+
const expansion = await definition.expandArgument(args, createHeadlessPaneContext(resolved, context, signal));
|
|
219
|
+
throwIfAborted(signal);
|
|
220
|
+
return expansion;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Loads the same renderer-neutral model used by `fn`.
|
|
225
|
+
* Both reports and screenshot payloads execute this loader.
|
|
226
|
+
*/
|
|
227
|
+
export async function loadResolvedHeadlessPaneModel(
|
|
228
|
+
resolved: ResolvedPaneFunction,
|
|
229
|
+
context: HeadlessMarketContext,
|
|
230
|
+
rawArgument: string,
|
|
231
|
+
signal: AbortSignal = new AbortController().signal,
|
|
232
|
+
): Promise<LoadedHeadlessPaneModel> {
|
|
233
|
+
const definition = resolved.headless;
|
|
234
|
+
if (!definition) throw new Error(`${resolved.token} has no headless pane model.`);
|
|
235
|
+
const args = buildHeadlessPaneLoadArgs(definition, resolved.token, rawArgument, resolved.options);
|
|
236
|
+
const result = await loadHeadlessPaneModel(definition, args, createHeadlessPaneContext(resolved, context, signal));
|
|
207
237
|
return { definition, args, result };
|
|
208
238
|
}
|
|
209
239
|
|
|
@@ -527,6 +557,36 @@ function splitErrors(result: HeadlessPaneResult): { locked: string[]; failed: st
|
|
|
527
557
|
return { locked: errors.filter(isProLock), failed: errors.filter((error) => !isProLock(error)) };
|
|
528
558
|
}
|
|
529
559
|
|
|
560
|
+
/**
|
|
561
|
+
* Why a report with nothing in it is empty, when its pane gave no reason of
|
|
562
|
+
* its own: the free view left it empty (the report carries a Pro line), or the
|
|
563
|
+
* symbols asked for are not covered by the function (or hold nothing for the
|
|
564
|
+
* options chosen). Null when something has
|
|
565
|
+
* rows, a table states its own reason, an error explains it, or no symbol was
|
|
566
|
+
* asked for (a board that is empty says nothing about coverage).
|
|
567
|
+
*/
|
|
568
|
+
function emptyReportReason(
|
|
569
|
+
definition: HeadlessPaneDefinition,
|
|
570
|
+
result: HeadlessPaneResult,
|
|
571
|
+
args: HeadlessPaneLoadArgs,
|
|
572
|
+
token: string | undefined,
|
|
573
|
+
): string | null {
|
|
574
|
+
if (resultRowCount(definition, result) > 0) return null;
|
|
575
|
+
const ownReason = definition.shape === "rows" ? !!(result as HeadlessRowsResult).empty
|
|
576
|
+
: definition.shape === "bundle" && (result as HeadlessBundleResult).sections.some((section) => "empty" in section && !!section.empty);
|
|
577
|
+
const { locked, failed } = splitErrors(result);
|
|
578
|
+
if (ownReason || failed.length) return null;
|
|
579
|
+
if (locked.length) return "Locked: Gloom Pro";
|
|
580
|
+
const symbols = result.symbols ?? args.symbols;
|
|
581
|
+
if (!symbols.length) return null;
|
|
582
|
+
// An option the reader chose (a range, a tab) can leave a covered symbol empty, so coverage is not claimed then.
|
|
583
|
+
const narrowed = definition.options.some((option) => {
|
|
584
|
+
const value = args.options[option.key];
|
|
585
|
+
return value !== undefined && value !== "" && String(value) !== String(option.defaultValue ?? "");
|
|
586
|
+
});
|
|
587
|
+
return narrowed ? `No data for ${symbols.join(", ")} with these options.` : `No data for ${symbols.join(", ")}: not covered${token ? ` by ${token}` : ""}.`;
|
|
588
|
+
}
|
|
589
|
+
|
|
530
590
|
function textNotices(result: HeadlessPaneResult): string[] {
|
|
531
591
|
const notices = result.metadata?.notices;
|
|
532
592
|
if (!Array.isArray(notices)) return [];
|
|
@@ -634,15 +694,20 @@ export function renderHeadlessPaneText(
|
|
|
634
694
|
args: HeadlessPaneLoadArgs,
|
|
635
695
|
fallbackTitle: string,
|
|
636
696
|
freshness: ReportFreshness = deriveHeadlessFreshness(definition, result),
|
|
697
|
+
/** The function's token, which an empty report names as the one that does not cover the symbol. */
|
|
698
|
+
token?: string,
|
|
637
699
|
): string {
|
|
638
700
|
// A long title wraps to a narrow --width like the text under a table; each line stays bold.
|
|
639
701
|
const lines = [...wrapToTerminal(reportTitle(definition, args, result, fallbackTitle)).split("\n").map(cliStyles.bold), ""];
|
|
640
702
|
const notices = textNotices(result);
|
|
641
703
|
if (notices.length) lines.push(...notices, "");
|
|
642
|
-
|
|
704
|
+
const reason = emptyReportReason(definition, result, args, token);
|
|
705
|
+
// Said once, not under the heading of each empty table.
|
|
706
|
+
if (reason) lines.push(reason);
|
|
707
|
+
else switch (definition.shape) {
|
|
643
708
|
case "rows": {
|
|
644
709
|
const rowsResult = result as HeadlessRowsResult;
|
|
645
|
-
lines.push(renderRows(rowsResult.rows, rowsResult.columns, definition.columns, rowsResult.empty));
|
|
710
|
+
lines.push(renderRows(rowsResult.rows, rowsResult.textColumns ?? rowsResult.columns, definition.columns, rowsResult.empty));
|
|
646
711
|
break;
|
|
647
712
|
}
|
|
648
713
|
case "bundle":
|
|
@@ -756,6 +821,7 @@ export async function buildHeadlessFunctionReport(
|
|
|
756
821
|
loaded.args,
|
|
757
822
|
resolved.label,
|
|
758
823
|
freshness,
|
|
824
|
+
resolved.token,
|
|
759
825
|
),
|
|
760
826
|
tables: headlessReportTables(
|
|
761
827
|
loaded.definition,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { extname, isAbsolute, relative, resolve } from "path";
|
|
2
|
-
import type { CliCommandContext } from "../../types/plugin";
|
|
2
|
+
import type { CliCommandContext, HeadlessArgumentExpansion } from "../../types/plugin";
|
|
3
3
|
import type { MarketContext } from "../types";
|
|
4
4
|
import { withMarketData } from "../context";
|
|
5
5
|
import { cliStyles, renderStats, type CliStatEntry } from "../../utils/cli-output";
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
findFunctionCode,
|
|
9
9
|
renderPaneCatalogReport,
|
|
10
10
|
selectPaneCatalogMatches,
|
|
11
|
+
suggestPaneFunctions,
|
|
11
12
|
type PaneFunctionCatalog,
|
|
12
13
|
} from "./catalog";
|
|
13
14
|
import { createPaneCatalog } from "./discovery";
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from "./options";
|
|
20
21
|
import { applyListingArgument, resolvePaneFunction, UnknownPaneFunctionError, type ResolvedPaneFunction } from "./resolver";
|
|
21
22
|
import { buildFunctionReport } from "./report";
|
|
23
|
+
import { expandResolvedArgument } from "./headless";
|
|
22
24
|
import { defaultScreenshotPath, renderDesktopShot, shotSizeWarnings, shotStatusIssues } from "./screenshot";
|
|
23
25
|
import {
|
|
24
26
|
buildPaneCatalogEntries,
|
|
@@ -29,6 +31,7 @@ import { ListingArgError, listingReadNotes, loadForListing, quotedListingChoiceN
|
|
|
29
31
|
import { knownGapMessage, settingsPointer, topicPointer } from "../known-gaps";
|
|
30
32
|
import { selectReportTables } from "../report-tables";
|
|
31
33
|
import { cliOutputWritten } from "../result";
|
|
34
|
+
import type { CatalogCommandHit } from "../unknown-command";
|
|
32
35
|
import { CLI_EXIT_CODES } from "../exit-codes";
|
|
33
36
|
import {
|
|
34
37
|
GLOSSARY,
|
|
@@ -55,8 +58,14 @@ async function withPaneRuntime<T>(
|
|
|
55
58
|
const context: MarketContext = ctx.cliOptions.refresh ? { ...market, refresh: true } : market;
|
|
56
59
|
const registry = await createPaneCatalog(context, ctx.plugins);
|
|
57
60
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
let parsed = await applyListingArgument(registry, context, parsePaneFunctionArgs(args, ctx.cliOptions), settings.command);
|
|
62
|
+
let resolved = await resolvePaneFunction(registry, context, parsed, settings);
|
|
63
|
+
// A function that widens its argument (RV's --peers) runs, reports and captures the widened one.
|
|
64
|
+
const expansion = await withPersistedCloudSession(context, () => expandResolvedArgument(resolved, context, parsed.arg));
|
|
65
|
+
if (expansion) {
|
|
66
|
+
parsed = { ...parsed, arg: expansion.argument };
|
|
67
|
+
resolved = withExpansion(await resolvePaneFunction(registry, context, parsed, settings), expansion);
|
|
68
|
+
}
|
|
60
69
|
return await run({ parsed, context, registry, resolved });
|
|
61
70
|
} finally {
|
|
62
71
|
registry.destroy();
|
|
@@ -64,6 +73,15 @@ async function withPaneRuntime<T>(
|
|
|
64
73
|
});
|
|
65
74
|
}
|
|
66
75
|
|
|
76
|
+
/** The resolved function with what its expansion adds to the pane: settings, and a title that says how. */
|
|
77
|
+
function withExpansion(resolved: ResolvedPaneFunction, { settings = {}, title }: HeadlessArgumentExpansion): ResolvedPaneFunction {
|
|
78
|
+
return {
|
|
79
|
+
...resolved,
|
|
80
|
+
optionSettings: { ...resolved.optionSettings, ...settings },
|
|
81
|
+
instance: { ...resolved.instance, ...(title ? { title } : {}), settings: { ...resolved.instance.settings, ...settings } },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
67
85
|
/** A word that can be a function code: letters and digits, with the dots and dashes some codes carry. */
|
|
68
86
|
const FUNCTION_CODE_WORD = /^[\w.$-]{1,24}$/;
|
|
69
87
|
|
|
@@ -102,6 +120,42 @@ export async function functionCodeCommand(
|
|
|
102
120
|
}
|
|
103
121
|
}
|
|
104
122
|
|
|
123
|
+
/** How many functions an unknown command word is answered with. */
|
|
124
|
+
const COMMAND_HIT_LIMIT = 3;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The functions the catalog finds for a word typed as a command (`gloomberb
|
|
128
|
+
* comps`): what `catalog <word>` would list first, as many as are close.
|
|
129
|
+
* Empty when the word finds none or the catalog cannot be read.
|
|
130
|
+
*/
|
|
131
|
+
export async function catalogCommandHits(ctx: CliCommandContext, token: string): Promise<CatalogCommandHit[]> {
|
|
132
|
+
const word = token.trim();
|
|
133
|
+
if (!/^[a-z][\w-]{1,30}$/i.test(word)) return [];
|
|
134
|
+
try {
|
|
135
|
+
return await withMarketData(ctx, async (market) => {
|
|
136
|
+
const registry = await createPaneCatalog(market, ctx.plugins);
|
|
137
|
+
try {
|
|
138
|
+
return (await suggestPaneFunctions(registry, market, word)).slice(0, COMMAND_HIT_LIMIT).map((entry) => ({
|
|
139
|
+
code: entry.token,
|
|
140
|
+
label: entry.label,
|
|
141
|
+
argKind: entry.argKind ?? null,
|
|
142
|
+
argPlaceholder: entry.argPlaceholder ?? null,
|
|
143
|
+
peers: entry.capability.options.some((option) => option.key === "peers" && option.type === "boolean"),
|
|
144
|
+
}));
|
|
145
|
+
} finally {
|
|
146
|
+
registry.destroy();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
} catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Whether a list report has no row with data: it is empty, or every row is a name without any. */
|
|
155
|
+
function foundNothing(data: { rowCount: number; unavailableSymbols: readonly string[] }): boolean {
|
|
156
|
+
return data.rowCount === 0 || data.unavailableSymbols.length >= data.rowCount;
|
|
157
|
+
}
|
|
158
|
+
|
|
105
159
|
export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
|
|
106
160
|
await runPaneCliCommand(ctx, async () => {
|
|
107
161
|
await withPaneRuntime(ctx, args, async ({ parsed, context, resolved }) => {
|
|
@@ -161,6 +215,8 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
|
|
|
161
215
|
text: () => glossary ? `${report.text}\n\n${renderReportGlossary(resolved.token, glossary)}` : report.text,
|
|
162
216
|
...(tabular ? { tables: selectReportTables(report.tables, resolved.tableSection) } : {}),
|
|
163
217
|
});
|
|
218
|
+
// The report is printed as it is; a list that found data for none of its names says so in the exit status too.
|
|
219
|
+
if (resolved.headless?.failsWithoutData && foundNothing(report.data)) process.exitCode = CLI_EXIT_CODES.error;
|
|
164
220
|
}, { strictHeadlessOptions: true, tableSection: true });
|
|
165
221
|
});
|
|
166
222
|
}
|
|
@@ -283,7 +339,7 @@ function runGlossary(term: string, ctx: CliCommandContext): void {
|
|
|
283
339
|
const { exact, partial } = lookupGlossary(term);
|
|
284
340
|
const entries = [...exact, ...partial];
|
|
285
341
|
if (entries.length === 0) {
|
|
286
|
-
ctx.fail(`No glossary entry for "${term}".`, "gloomberb catalog glossary lists every term.");
|
|
342
|
+
ctx.fail(`No glossary entry for "${term}".`, knownGapMessage(term) ?? "gloomberb catalog glossary lists every term.");
|
|
287
343
|
}
|
|
288
344
|
ctx.printResult({ data: entries.map(glossaryRecord) }, { text: () => renderGlossaryEntries(entries).join("\n") });
|
|
289
345
|
}
|