@insightsentry/mcp 1.4.1 → 1.4.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.
@@ -0,0 +1,625 @@
1
+ import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const HISTORY_PATH = "/v3/symbols/{symbol}/history";
4
+ const SERIES_PATH = "/v3/symbols/{symbol}/series";
5
+ const CONTRACTS_PATH = "/v3/symbols/{symbol}/contracts";
6
+ const DEFAULT_CONCURRENCY = 5;
7
+ const MAX_CONCURRENCY = 10;
8
+ const DEFAULT_CONTRACT_LOOKBACK_MONTHS = 6;
9
+ const HISTORY_BAR_TYPES = ["second", "minute", "hour"];
10
+ const SERIES_BAR_TYPES = ["day", "week", "month"];
11
+ export async function planHistoryRequests(options, deps) {
12
+ validateOptions(options);
13
+ if (isContinuousFrontMonth(options.symbol) && isArchiveBarType(options.bar_type)) {
14
+ return planFuturesHistoryRequests(options, deps.request);
15
+ }
16
+ return {
17
+ mode: "regular",
18
+ requests: planRequestsForSymbol(options, options.symbol),
19
+ };
20
+ }
21
+ export async function downloadHistory(options, deps) {
22
+ const format = options.format ?? "csv";
23
+ const concurrency = normalizeConcurrency(options.concurrency);
24
+ const shouldMergeCsv = options.merge ?? true;
25
+ const plan = await planHistoryRequests(options, deps);
26
+ const mergeCsvFilesByRequestIndex = new Map();
27
+ const createdCsvFiles = new Set();
28
+ const result = {
29
+ mode: plan.mode,
30
+ total: plan.requests.length,
31
+ completed: 0,
32
+ skipped: 0,
33
+ failed: 0,
34
+ concurrency,
35
+ output_dir: path.resolve(options.output_dir),
36
+ files: [],
37
+ errors: [],
38
+ };
39
+ let nextIndex = 0;
40
+ async function worker() {
41
+ while (nextIndex < plan.requests.length) {
42
+ const requestIndex = nextIndex;
43
+ const request = plan.requests[requestIndex];
44
+ nextIndex += 1;
45
+ const savedFiles = [];
46
+ const startDate = String(request.params.start_date ?? `${options.from}_${options.to}`);
47
+ const symbol = String(request.params.symbol);
48
+ try {
49
+ const existingTargetFiles = !options.overwrite
50
+ ? await findExistingTargetFiles(request.outputBasePath, format)
51
+ : null;
52
+ if (existingTargetFiles) {
53
+ mergeCsvFilesByRequestIndex.set(requestIndex, existingTargetFiles.filter((target) => target.endsWith(".csv")));
54
+ result.skipped += 1;
55
+ result.completed += 1;
56
+ deps.onProgress?.({
57
+ completed: result.completed,
58
+ total: result.total,
59
+ status: "skipped",
60
+ symbol,
61
+ start_date: startDate,
62
+ files: existingTargetFiles,
63
+ });
64
+ continue;
65
+ }
66
+ const response = filterResponseForRequest(await deps.request("GET", request.method === "series" ? SERIES_PATH : HISTORY_PATH, request.params), options, request);
67
+ const message = response?.message ?? response?.error;
68
+ if (message && !Array.isArray(response?.series)) {
69
+ result.skipped += 1;
70
+ result.completed += 1;
71
+ deps.onProgress?.({
72
+ completed: result.completed,
73
+ total: result.total,
74
+ status: "skipped",
75
+ symbol,
76
+ start_date: startDate,
77
+ files: [],
78
+ error: String(message),
79
+ });
80
+ continue;
81
+ }
82
+ const outputBasePath = outputBasePathForResponse(request, response);
83
+ const targetFiles = outputFilesForFormat(outputBasePath, format);
84
+ if (!options.overwrite && (await allFilesExist(targetFiles))) {
85
+ mergeCsvFilesByRequestIndex.set(requestIndex, targetFiles.filter((target) => target.endsWith(".csv")));
86
+ result.skipped += 1;
87
+ result.completed += 1;
88
+ deps.onProgress?.({
89
+ completed: result.completed,
90
+ total: result.total,
91
+ status: "skipped",
92
+ symbol,
93
+ start_date: startDate,
94
+ files: targetFiles,
95
+ });
96
+ continue;
97
+ }
98
+ for (const filePath of targetFiles) {
99
+ await mkdir(path.dirname(filePath), { recursive: true });
100
+ if (filePath.endsWith(".csv")) {
101
+ await writeFile(filePath, responseToCsv(response), "utf8");
102
+ }
103
+ else {
104
+ await writeFile(filePath, `${JSON.stringify(response, null, 2)}\n`, "utf8");
105
+ }
106
+ savedFiles.push(filePath);
107
+ result.files.push(filePath);
108
+ if (filePath.endsWith(".csv")) {
109
+ const csvFiles = mergeCsvFilesByRequestIndex.get(requestIndex) ?? [];
110
+ csvFiles.push(filePath);
111
+ mergeCsvFilesByRequestIndex.set(requestIndex, csvFiles);
112
+ createdCsvFiles.add(filePath);
113
+ }
114
+ }
115
+ result.completed += 1;
116
+ deps.onProgress?.({
117
+ completed: result.completed,
118
+ total: result.total,
119
+ status: "saved",
120
+ symbol,
121
+ start_date: startDate,
122
+ files: savedFiles,
123
+ });
124
+ }
125
+ catch (error) {
126
+ const message = error?.message ?? String(error);
127
+ result.failed += 1;
128
+ result.completed += 1;
129
+ result.errors.push({ symbol, start_date: startDate, message });
130
+ deps.onProgress?.({
131
+ completed: result.completed,
132
+ total: result.total,
133
+ status: "failed",
134
+ symbol,
135
+ start_date: startDate,
136
+ files: savedFiles,
137
+ error: message,
138
+ });
139
+ }
140
+ }
141
+ }
142
+ await Promise.all(Array.from({ length: Math.min(concurrency, plan.requests.length) }, () => worker()));
143
+ if (shouldMergeCsv && (format === "csv" || format === "both")) {
144
+ const orderedCsvFiles = plan.requests.flatMap((_request, index) => mergeCsvFilesByRequestIndex.get(index) ?? []);
145
+ if (orderedCsvFiles.length > 0) {
146
+ const mergedFile = mergedCsvPath(options, path.basename(path.dirname(orderedCsvFiles[0])));
147
+ await mergeCsvFiles(orderedCsvFiles, mergedFile);
148
+ result.merged_file = mergedFile;
149
+ result.files.push(mergedFile);
150
+ if (!options.keep_chunks) {
151
+ const removableCsvFiles = orderedCsvFiles.filter((filePath) => createdCsvFiles.has(filePath));
152
+ await removeChunkFiles(removableCsvFiles);
153
+ result.files = result.files.filter((filePath) => !removableCsvFiles.includes(filePath));
154
+ }
155
+ }
156
+ }
157
+ return result;
158
+ }
159
+ export function responseToCsv(response) {
160
+ const series = Array.isArray(response?.series) ? response.series : [];
161
+ if (series.length === 0)
162
+ return "";
163
+ const metadataHeaders = response?.code && response?.bar_type ? ["code", "bar_type"] : [];
164
+ const metadataValues = metadataHeaders.map((key) => response[key]);
165
+ if (Array.isArray(response?.series_keys) && Array.isArray(series[0])) {
166
+ const headers = [...metadataHeaders, ...response.series_keys.map(String)];
167
+ const rows = series.map((row) => [...metadataValues, ...row]);
168
+ return serializeCsvRows(headers, rows);
169
+ }
170
+ const preferred = ["code", "bar_type", "time", "open", "high", "low", "close", "volume", "type"];
171
+ const keys = new Set();
172
+ for (const row of series) {
173
+ if (row && typeof row === "object" && !Array.isArray(row)) {
174
+ for (const key of Object.keys(row))
175
+ keys.add(key);
176
+ }
177
+ }
178
+ for (const key of metadataHeaders)
179
+ keys.add(key);
180
+ const headers = [...preferred.filter((key) => keys.delete(key)), ...Array.from(keys).sort()];
181
+ const rows = series.map((row) => headers.map((key) => (key in row ? row[key] : response?.[key])));
182
+ return serializeCsvRows(headers, rows);
183
+ }
184
+ function planRequestsForSymbol(options, symbol, outputSymbol = symbol) {
185
+ if (isSeriesBarType(options.bar_type)) {
186
+ const rangeStart = parseDay(options.from, "start");
187
+ const rangeEnd = parseDay(options.to, "end");
188
+ if (compareDays(rangeStart, rangeEnd) > 0)
189
+ throw new Error("from must be before or equal to to");
190
+ const rangeLabel = `${formatDay(rangeStart)}_${formatDay(rangeEnd)}`;
191
+ return [
192
+ {
193
+ method: "series",
194
+ params: {
195
+ symbol,
196
+ bar_type: options.bar_type,
197
+ dp: 30000,
198
+ ...optionalSeriesParams(options),
199
+ },
200
+ outputBasePath: path.join(path.resolve(options.output_dir), sanitizePathPart(outputSymbol), timeframeLabel(options.bar_type, options.bar_interval), rangeLabel),
201
+ },
202
+ ];
203
+ }
204
+ const startDates = iterStartDates(options.from, options.to, options.bar_type);
205
+ return startDates.map((startDate) => ({
206
+ method: "history",
207
+ params: {
208
+ symbol,
209
+ bar_type: options.bar_type,
210
+ start_date: startDate,
211
+ ...optionalHistoryParams(options),
212
+ },
213
+ outputBasePath: path.join(path.resolve(options.output_dir), sanitizePathPart(outputSymbol), timeframeLabel(options.bar_type, options.bar_interval), startDate),
214
+ }));
215
+ }
216
+ async function planFuturesHistoryRequests(options, request) {
217
+ const schedule = await fetchContractSchedule(options.symbol, request);
218
+ const range = monthRangeFor(options.from, options.to);
219
+ const lookback = options.contract_lookback_months ?? DEFAULT_CONTRACT_LOOKBACK_MONTHS;
220
+ const requests = [];
221
+ for (let year = range.start.year; year <= range.end.year + 1; year += 1) {
222
+ for (const monthCode of schedule.monthCodes) {
223
+ const settlementMonth = schedule.settlementMonths.get(monthCode);
224
+ if (!settlementMonth)
225
+ continue;
226
+ const settlement = { year, month: settlementMonth };
227
+ const coverageStart = addMonths(settlement, -lookback + 1);
228
+ for (const month of iterMonths(coverageStart, settlement)) {
229
+ if (compareMonths(month, range.start) < 0 || compareMonths(month, range.end) > 0) {
230
+ continue;
231
+ }
232
+ const symbol = `${schedule.fullBaseCode}${monthCode}${year}`;
233
+ const startDate = formatMonth(month);
234
+ for (const archiveDate of expandArchiveStartDates(startDate, options.bar_type)) {
235
+ requests.push({
236
+ method: "history",
237
+ params: {
238
+ symbol,
239
+ bar_type: options.bar_type,
240
+ start_date: archiveDate,
241
+ ...optionalHistoryParams(options),
242
+ },
243
+ outputBasePath: path.join(path.resolve(options.output_dir), sanitizePathPart(schedule.fullBaseCode), sanitizePathPart(symbol), timeframeLabel(options.bar_type, options.bar_interval), archiveDate),
244
+ });
245
+ }
246
+ }
247
+ }
248
+ }
249
+ return { mode: "futures", requests };
250
+ }
251
+ async function fetchContractSchedule(symbol, request) {
252
+ const response = await request("GET", CONTRACTS_PATH, { symbol });
253
+ const inferredBase = continuousToBaseSymbol(symbol);
254
+ const rawBase = String(response?.base_code || inferredBase);
255
+ const fullBaseCode = rawBase.includes(":") ? rawBase : `${symbol.split(":", 1)[0]}:${rawBase}`;
256
+ const rootBase = fullBaseCode.includes(":")
257
+ ? (fullBaseCode.split(":").at(-1) ?? fullBaseCode)
258
+ : fullBaseCode;
259
+ const settlementMonths = new Map();
260
+ for (const contract of response?.contracts ?? []) {
261
+ const code = String(contract?.code ?? "");
262
+ const settlementDate = String(contract?.settlement_date ?? "");
263
+ const monthCode = extractMonthCode(code, fullBaseCode, rootBase);
264
+ const month = parseSettlementMonth(settlementDate);
265
+ if (!monthCode || !month)
266
+ continue;
267
+ const existing = settlementMonths.get(monthCode);
268
+ if (existing !== undefined && existing !== month) {
269
+ throw new Error(`Contract month ${monthCode} maps to both ${existing} and ${month}`);
270
+ }
271
+ settlementMonths.set(monthCode, month);
272
+ }
273
+ if (settlementMonths.size === 0) {
274
+ throw new Error(`No contract schedule returned for ${symbol}`);
275
+ }
276
+ const monthCodes = Array.from(settlementMonths.entries())
277
+ .sort((a, b) => a[1] - b[1])
278
+ .map(([monthCode]) => monthCode);
279
+ return { fullBaseCode, monthCodes, settlementMonths };
280
+ }
281
+ function optionalHistoryParams(options) {
282
+ const params = {};
283
+ for (const key of ["bar_interval", "extended", "dadj", "badj", "settlement"]) {
284
+ if (options[key] !== undefined)
285
+ params[key] = options[key];
286
+ }
287
+ return params;
288
+ }
289
+ function optionalSeriesParams(options) {
290
+ return optionalHistoryParams(options);
291
+ }
292
+ function validateOptions(options) {
293
+ if (!options.symbol)
294
+ throw new Error("symbol is required");
295
+ if (!options.from)
296
+ throw new Error("from is required");
297
+ if (!options.to)
298
+ throw new Error("to is required");
299
+ if (!options.output_dir)
300
+ throw new Error("output_dir is required");
301
+ if (![...HISTORY_BAR_TYPES, ...SERIES_BAR_TYPES].includes(options.bar_type)) {
302
+ throw new Error("bar_type must be second, minute, hour, day, week, or month");
303
+ }
304
+ normalizeConcurrency(options.concurrency);
305
+ if (options.format && !["json", "csv", "both"].includes(options.format)) {
306
+ throw new Error("format must be json, csv, or both");
307
+ }
308
+ if (options.bar_interval !== undefined &&
309
+ (!Number.isInteger(options.bar_interval) ||
310
+ options.bar_interval < 1 ||
311
+ options.bar_interval > 1440)) {
312
+ throw new Error("bar_interval must be an integer between 1 and 1440");
313
+ }
314
+ if (options.contract_lookback_months !== undefined &&
315
+ (!Number.isInteger(options.contract_lookback_months) || options.contract_lookback_months < 1)) {
316
+ throw new Error("contract_lookback_months must be a positive integer");
317
+ }
318
+ }
319
+ function normalizeConcurrency(value) {
320
+ const concurrency = value ?? DEFAULT_CONCURRENCY;
321
+ if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > MAX_CONCURRENCY) {
322
+ throw new Error(`concurrency must be an integer between 1 and ${MAX_CONCURRENCY}`);
323
+ }
324
+ return concurrency;
325
+ }
326
+ function isContinuousFrontMonth(symbol) {
327
+ return symbol.endsWith("1!") || symbol.endsWith("2!");
328
+ }
329
+ function isArchiveBarType(barType) {
330
+ return HISTORY_BAR_TYPES.includes(barType);
331
+ }
332
+ function isSeriesBarType(barType) {
333
+ return SERIES_BAR_TYPES.includes(barType);
334
+ }
335
+ function continuousToBaseSymbol(symbol) {
336
+ return symbol.endsWith("1!") || symbol.endsWith("2!") ? symbol.slice(0, -2) : symbol;
337
+ }
338
+ function iterStartDates(from, to, barType) {
339
+ if (barType === "second") {
340
+ const start = parseDay(from, "start");
341
+ const end = parseDay(to, "end");
342
+ if (compareDays(start, end) > 0)
343
+ throw new Error("from must be before or equal to to");
344
+ return Array.from(iterDays(start, end), formatDay);
345
+ }
346
+ const { start, end } = monthRangeFor(from, to);
347
+ if (compareMonths(start, end) > 0)
348
+ throw new Error("from must be before or equal to to");
349
+ return Array.from(iterMonths(start, end), formatMonth);
350
+ }
351
+ function monthRangeFor(from, to) {
352
+ return { start: parseMonth(from), end: parseMonth(to) };
353
+ }
354
+ function expandArchiveStartDates(month, barType) {
355
+ if (barType !== "second")
356
+ return [month];
357
+ const start = parseDay(month, "start");
358
+ const end = parseDay(month, "end");
359
+ return Array.from(iterDays(start, end), formatDay);
360
+ }
361
+ function parseMonth(value) {
362
+ const match = /^(?<year>\d{4})-(?<month>\d{2})(?:-(?<day>\d{2}))?$/.exec(value);
363
+ if (!match?.groups)
364
+ throw new Error(`Invalid date "${value}". Use YYYY-MM or YYYY-MM-DD.`);
365
+ const year = Number(match.groups.year);
366
+ const month = Number(match.groups.month);
367
+ if (month < 1 || month > 12)
368
+ throw new Error(`Invalid month in "${value}"`);
369
+ if (match.groups.day !== undefined) {
370
+ const day = Number(match.groups.day);
371
+ if (day < 1 || day > daysInMonth(year, month)) {
372
+ throw new Error(`Invalid day in "${value}"`);
373
+ }
374
+ }
375
+ return { year, month };
376
+ }
377
+ function parseDay(value, bound) {
378
+ const monthOnly = /^(?<year>\d{4})-(?<month>\d{2})$/.exec(value);
379
+ if (monthOnly?.groups) {
380
+ const year = Number(monthOnly.groups.year);
381
+ const month = Number(monthOnly.groups.month);
382
+ if (month < 1 || month > 12)
383
+ throw new Error(`Invalid month in "${value}"`);
384
+ const day = bound === "start" ? 1 : daysInMonth(year, month);
385
+ return { year, month, day };
386
+ }
387
+ const match = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/.exec(value);
388
+ if (!match?.groups)
389
+ throw new Error(`Invalid date "${value}". Use YYYY-MM-DD.`);
390
+ const year = Number(match.groups.year);
391
+ const month = Number(match.groups.month);
392
+ const day = Number(match.groups.day);
393
+ if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {
394
+ throw new Error(`Invalid day in "${value}"`);
395
+ }
396
+ return { year, month, day };
397
+ }
398
+ function* iterMonths(start, end) {
399
+ let current = start;
400
+ while (compareMonths(current, end) <= 0) {
401
+ yield current;
402
+ current = addMonths(current, 1);
403
+ }
404
+ }
405
+ function* iterDays(start, end) {
406
+ let current = start;
407
+ while (compareDays(current, end) <= 0) {
408
+ yield current;
409
+ current = addDays(current, 1);
410
+ }
411
+ }
412
+ function addMonths(value, months) {
413
+ const index = value.year * 12 + value.month - 1 + months;
414
+ return { year: Math.floor(index / 12), month: (index % 12) + 1 };
415
+ }
416
+ function addDays(value, days) {
417
+ const date = new Date(Date.UTC(value.year, value.month - 1, value.day + days));
418
+ return { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate() };
419
+ }
420
+ function compareMonths(a, b) {
421
+ return a.year === b.year ? a.month - b.month : a.year - b.year;
422
+ }
423
+ function compareDays(a, b) {
424
+ if (a.year !== b.year)
425
+ return a.year - b.year;
426
+ if (a.month !== b.month)
427
+ return a.month - b.month;
428
+ return a.day - b.day;
429
+ }
430
+ function formatMonth(value) {
431
+ return `${value.year}-${String(value.month).padStart(2, "0")}`;
432
+ }
433
+ function formatDay(value) {
434
+ return `${formatMonth(value)}-${String(value.day).padStart(2, "0")}`;
435
+ }
436
+ function daysInMonth(year, month) {
437
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
438
+ }
439
+ function timeframeLabel(barType, interval = 1) {
440
+ const suffix = { second: "s", minute: "m", hour: "h", day: "D", week: "W", month: "M" }[barType];
441
+ return `${interval}${suffix}`;
442
+ }
443
+ function filterResponseForRequest(response, options, request) {
444
+ if (request.method !== "series" || !Array.isArray(response?.series))
445
+ return response;
446
+ const start = dayBoundaryUnixSeconds(parseDay(options.from, "start"), "start");
447
+ const end = dayBoundaryUnixSeconds(parseDay(options.to, "end"), "end");
448
+ return {
449
+ ...response,
450
+ series: response.series.filter((row) => {
451
+ const time = Array.isArray(row) ? row[0] : row?.time;
452
+ return typeof time === "number" && time >= start && time <= end;
453
+ }),
454
+ };
455
+ }
456
+ function dayBoundaryUnixSeconds(value, bound) {
457
+ const milliseconds = bound === "start"
458
+ ? Date.UTC(value.year, value.month - 1, value.day, 0, 0, 0, 0)
459
+ : Date.UTC(value.year, value.month - 1, value.day, 23, 59, 59, 999);
460
+ return Math.floor(milliseconds / 1000);
461
+ }
462
+ function sanitizePathPart(value) {
463
+ return value.replace(/[^A-Za-z0-9._!-]+/g, "_");
464
+ }
465
+ function outputFilesForFormat(outputBasePath, format) {
466
+ if (format === "both")
467
+ return [`${outputBasePath}.json`, `${outputBasePath}.csv`];
468
+ return [`${outputBasePath}.${format}`];
469
+ }
470
+ function outputBasePathForResponse(request, response) {
471
+ const responseBarType = typeof response?.bar_type === "string" && response.bar_type.trim()
472
+ ? sanitizePathPart(response.bar_type.trim())
473
+ : path.basename(path.dirname(request.outputBasePath));
474
+ return path.join(path.dirname(path.dirname(request.outputBasePath)), responseBarType, path.basename(request.outputBasePath));
475
+ }
476
+ function mergedCsvPath(options, barTypeLabel) {
477
+ return path.join(path.resolve(options.output_dir), sanitizePathPart(options.symbol), sanitizePathPart(barTypeLabel), "merged.csv");
478
+ }
479
+ async function mergeCsvFiles(files, outputPath) {
480
+ await mkdir(path.dirname(outputPath), { recursive: true });
481
+ const headers = [];
482
+ const headerSet = new Set();
483
+ const rowsByKey = new Map();
484
+ for (const file of files) {
485
+ const content = await readFile(file, "utf8");
486
+ if (!content.trim())
487
+ continue;
488
+ const lines = content.split(/\r?\n/).filter((line) => line.length > 0);
489
+ if (lines.length === 0)
490
+ continue;
491
+ const columns = parseCsvLine(lines[0]);
492
+ const codeIndex = columns.indexOf("code");
493
+ const barTypeIndex = columns.indexOf("bar_type");
494
+ const timeIndex = columns.indexOf("time");
495
+ if (codeIndex === -1 || barTypeIndex === -1 || timeIndex === -1) {
496
+ throw new Error("Merged CSV requires code, bar_type, and time columns for deduplication");
497
+ }
498
+ for (const column of columns) {
499
+ if (!headerSet.has(column)) {
500
+ headerSet.add(column);
501
+ headers.push(column);
502
+ }
503
+ }
504
+ for (const line of lines.slice(1)) {
505
+ const values = parseCsvLine(line);
506
+ const key = [codeIndex, barTypeIndex, timeIndex]
507
+ .map((index) => values[index] ?? "")
508
+ .join("\u0000");
509
+ const row = {};
510
+ for (let index = 0; index < columns.length; index += 1) {
511
+ row[columns[index]] = values[index] ?? "";
512
+ }
513
+ rowsByKey.set(key, row);
514
+ }
515
+ }
516
+ const rows = Array.from(rowsByKey.values()).map((row) => headers.map((header) => row[header] ?? ""));
517
+ const content = headers.length > 0 ? serializeCsvRows(headers, rows) : "";
518
+ await writeFile(outputPath, content, "utf8");
519
+ }
520
+ async function allFilesExist(files) {
521
+ for (const file of files) {
522
+ try {
523
+ await readFile(file);
524
+ }
525
+ catch {
526
+ return false;
527
+ }
528
+ }
529
+ return true;
530
+ }
531
+ async function findExistingTargetFiles(outputBasePath, format) {
532
+ const outputRoot = path.dirname(path.dirname(outputBasePath));
533
+ const outputName = path.basename(outputBasePath);
534
+ let entries;
535
+ try {
536
+ entries = await readdir(outputRoot, { withFileTypes: true });
537
+ }
538
+ catch (error) {
539
+ if (error?.code === "ENOENT")
540
+ return null;
541
+ throw error;
542
+ }
543
+ for (const entry of entries) {
544
+ if (!entry.isDirectory())
545
+ continue;
546
+ const candidateFiles = outputFilesForFormat(path.join(outputRoot, entry.name, outputName), format);
547
+ if (await allFilesExist(candidateFiles))
548
+ return candidateFiles;
549
+ }
550
+ return null;
551
+ }
552
+ async function removeChunkFiles(files) {
553
+ await Promise.all(files.map(async (file) => {
554
+ try {
555
+ await unlink(file);
556
+ }
557
+ catch (error) {
558
+ if (error?.code !== "ENOENT")
559
+ throw error;
560
+ }
561
+ }));
562
+ }
563
+ function serializeCsvRows(headers, rows) {
564
+ const lines = [headers.map(escapeCsvCell).join(",")];
565
+ for (const row of rows) {
566
+ lines.push(row.map(escapeCsvCell).join(","));
567
+ }
568
+ return `${lines.join("\n")}\n`;
569
+ }
570
+ function escapeCsvCell(value) {
571
+ if (value === undefined || value === null)
572
+ return "";
573
+ const text = typeof value === "object" ? JSON.stringify(value) : String(value);
574
+ if (/[",\n\r]/.test(text)) {
575
+ return `"${text.replace(/"/g, '""')}"`;
576
+ }
577
+ return text;
578
+ }
579
+ function parseCsvLine(line) {
580
+ const cells = [];
581
+ let current = "";
582
+ let inQuotes = false;
583
+ for (let index = 0; index < line.length; index += 1) {
584
+ const char = line[index];
585
+ if (char === '"') {
586
+ if (inQuotes && line[index + 1] === '"') {
587
+ current += '"';
588
+ index += 1;
589
+ }
590
+ else {
591
+ inQuotes = !inQuotes;
592
+ }
593
+ continue;
594
+ }
595
+ if (char === "," && !inQuotes) {
596
+ cells.push(current);
597
+ current = "";
598
+ continue;
599
+ }
600
+ current += char;
601
+ }
602
+ cells.push(current);
603
+ return cells;
604
+ }
605
+ function parseSettlementMonth(value) {
606
+ const match = /^\d{4}(?<month>\d{2})\d{2}$/.exec(value);
607
+ if (!match?.groups)
608
+ return null;
609
+ const month = Number(match.groups.month);
610
+ return month >= 1 && month <= 12 ? month : null;
611
+ }
612
+ function extractMonthCode(code, fullBaseCode, rootBase) {
613
+ const prefixes = [fullBaseCode, rootBase];
614
+ for (const prefix of prefixes) {
615
+ if (!code.startsWith(prefix))
616
+ continue;
617
+ const suffix = code.slice(prefix.length);
618
+ const match = /^(?<monthCode>[A-Z])\d{4}$/.exec(suffix);
619
+ if (match?.groups)
620
+ return match.groups.monthCode;
621
+ }
622
+ const fallback = /(?<monthCode>[A-Z])\d{4}$/.exec(code);
623
+ return fallback?.groups?.monthCode ?? null;
624
+ }
625
+ //# sourceMappingURL=history.js.map