@transcend-io/cli 10.2.1 → 10.2.2
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/dist/{app-DLzaZHc8.mjs → app-DcwjuHHn.mjs} +16 -16
- package/dist/{app-DLzaZHc8.mjs.map → app-DcwjuHHn.mjs.map} +1 -1
- package/dist/{approvePrivacyRequests-BlUcYXpH.mjs → approvePrivacyRequests-vTXeO7hC.mjs} +2 -2
- package/dist/{approvePrivacyRequests-BlUcYXpH.mjs.map → approvePrivacyRequests-vTXeO7hC.mjs.map} +1 -1
- package/dist/bin/bash-complete.mjs +1 -1
- package/dist/bin/cli.mjs +1 -1
- package/dist/bin/deprecated-command.mjs +1 -1
- package/dist/{buildXdiSyncEndpoint-D5GxPH6o.mjs → buildXdiSyncEndpoint-Bd9SRD1A.mjs} +2 -2
- package/dist/{buildXdiSyncEndpoint-D5GxPH6o.mjs.map → buildXdiSyncEndpoint-Bd9SRD1A.mjs.map} +1 -1
- package/dist/{bulkRestartRequests-DILDBdc1.mjs → bulkRestartRequests-BvURrcOl.mjs} +2 -2
- package/dist/{bulkRestartRequests-DILDBdc1.mjs.map → bulkRestartRequests-BvURrcOl.mjs.map} +1 -1
- package/dist/{bulkRetryEnrichers-CjSz1472.mjs → bulkRetryEnrichers-CTbBdTEm.mjs} +2 -2
- package/dist/{bulkRetryEnrichers-CjSz1472.mjs.map → bulkRetryEnrichers-CTbBdTEm.mjs.map} +1 -1
- package/dist/{cancelPrivacyRequests-BWJZmZVY.mjs → cancelPrivacyRequests-p9OEzUXv.mjs} +2 -2
- package/dist/{cancelPrivacyRequests-BWJZmZVY.mjs.map → cancelPrivacyRequests-p9OEzUXv.mjs.map} +1 -1
- package/dist/{command-BMa3UWax.mjs → command-CuxgABlk.mjs} +2 -2
- package/dist/{command-BMa3UWax.mjs.map → command-CuxgABlk.mjs.map} +1 -1
- package/dist/{constants-TpID7AXE.mjs → constants-D22_ckyl.mjs} +2 -2
- package/dist/{constants-TpID7AXE.mjs.map → constants-D22_ckyl.mjs.map} +1 -1
- package/dist/{createExtraKeyHandler-BO4lu0HO.mjs → createExtraKeyHandler-BkfSV_aF.mjs} +2 -2
- package/dist/{createExtraKeyHandler-BO4lu0HO.mjs.map → createExtraKeyHandler-BkfSV_aF.mjs.map} +1 -1
- package/dist/{downloadPrivacyRequestFiles-8DtRUNXp.mjs → downloadPrivacyRequestFiles-CqHT6HSU.mjs} +2 -2
- package/dist/{downloadPrivacyRequestFiles-8DtRUNXp.mjs.map → downloadPrivacyRequestFiles-CqHT6HSU.mjs.map} +1 -1
- package/dist/{generateCrossAccountApiKeys-D6hg9146.mjs → generateCrossAccountApiKeys-Cj3YCdZN.mjs} +2 -2
- package/dist/{generateCrossAccountApiKeys-D6hg9146.mjs.map → generateCrossAccountApiKeys-Cj3YCdZN.mjs.map} +1 -1
- package/dist/{impl-C71CkarV.mjs → impl-0w7yd5pK.mjs} +2 -2
- package/dist/{impl-C71CkarV.mjs.map → impl-0w7yd5pK.mjs.map} +1 -1
- package/dist/{impl-DmQAAT-u.mjs → impl-122G24x5.mjs} +2 -2
- package/dist/{impl-DmQAAT-u.mjs.map → impl-122G24x5.mjs.map} +1 -1
- package/dist/{impl--VlanXjT.mjs → impl-56wfH4jn.mjs} +2 -2
- package/dist/{impl--VlanXjT.mjs.map → impl-56wfH4jn.mjs.map} +1 -1
- package/dist/{impl-DTaM3UE3.mjs → impl-7rVYG2LQ.mjs} +2 -2
- package/dist/{impl-DTaM3UE3.mjs.map → impl-7rVYG2LQ.mjs.map} +1 -1
- package/dist/{impl-BaHZqboi.mjs → impl-B1YGN9Iu.mjs} +2 -2
- package/dist/{impl-BaHZqboi.mjs.map → impl-B1YGN9Iu.mjs.map} +1 -1
- package/dist/{impl-Du8quB1O.mjs → impl-BF0jh34q.mjs} +2 -2
- package/dist/{impl-Du8quB1O.mjs.map → impl-BF0jh34q.mjs.map} +1 -1
- package/dist/{impl-BECek1in.mjs → impl-BJTCOmGC.mjs} +2 -2
- package/dist/{impl-BECek1in.mjs.map → impl-BJTCOmGC.mjs.map} +1 -1
- package/dist/{impl-Dik9I7Bz.mjs → impl-BPqs_ltg.mjs} +2 -2
- package/dist/{impl-Dik9I7Bz.mjs.map → impl-BPqs_ltg.mjs.map} +1 -1
- package/dist/{impl-OxHej0UO.mjs → impl-BdafaDmy.mjs} +2 -2
- package/dist/{impl-OxHej0UO.mjs.map → impl-BdafaDmy.mjs.map} +1 -1
- package/dist/{impl-BuvbXmXj.mjs → impl-BnRNJ3pm.mjs} +2 -2
- package/dist/{impl-BuvbXmXj.mjs.map → impl-BnRNJ3pm.mjs.map} +1 -1
- package/dist/{impl-DAu079Yl.mjs → impl-Bu8p8dqt.mjs} +2 -2
- package/dist/{impl-DAu079Yl.mjs.map → impl-Bu8p8dqt.mjs.map} +1 -1
- package/dist/{impl-BTZOd3VN.mjs → impl-BzWh_Xg7.mjs} +2 -2
- package/dist/{impl-BTZOd3VN.mjs.map → impl-BzWh_Xg7.mjs.map} +1 -1
- package/dist/{impl-DbGCApR_.mjs → impl-C-1-lvCm.mjs} +2 -2
- package/dist/{impl-DbGCApR_.mjs.map → impl-C-1-lvCm.mjs.map} +1 -1
- package/dist/{impl-BC17WMY4.mjs → impl-CXBLyoPL.mjs} +2 -2
- package/dist/{impl-BC17WMY4.mjs.map → impl-CXBLyoPL.mjs.map} +1 -1
- package/dist/{impl-CR6tW9Jz.mjs → impl-CZrCBjBJ.mjs} +2 -2
- package/dist/{impl-CR6tW9Jz.mjs.map → impl-CZrCBjBJ.mjs.map} +1 -1
- package/dist/{impl-BKvcmB7W.mjs → impl-Cf-R4Ale.mjs} +2 -2
- package/dist/{impl-BKvcmB7W.mjs.map → impl-Cf-R4Ale.mjs.map} +1 -1
- package/dist/{impl-DWiE5RsV.mjs → impl-Ci8n7Ohd.mjs} +2 -2
- package/dist/{impl-DWiE5RsV.mjs.map → impl-Ci8n7Ohd.mjs.map} +1 -1
- package/dist/{impl-CYS38cQM.mjs → impl-CruHl43V.mjs} +2 -2
- package/dist/{impl-CYS38cQM.mjs.map → impl-CruHl43V.mjs.map} +1 -1
- package/dist/{impl-c7VvcNpZ.mjs → impl-Cziyty3N.mjs} +2 -2
- package/dist/{impl-c7VvcNpZ.mjs.map → impl-Cziyty3N.mjs.map} +1 -1
- package/dist/{impl-CxwEMQhw.mjs → impl-D3XO6Mas.mjs} +2 -2
- package/dist/{impl-CxwEMQhw.mjs.map → impl-D3XO6Mas.mjs.map} +1 -1
- package/dist/{impl-DpuPyy-w.mjs → impl-D50QXpV8.mjs} +2 -2
- package/dist/{impl-DpuPyy-w.mjs.map → impl-D50QXpV8.mjs.map} +1 -1
- package/dist/{impl-CIYSnaMG.mjs → impl-DCS69D4u.mjs} +2 -2
- package/dist/{impl-CIYSnaMG.mjs.map → impl-DCS69D4u.mjs.map} +1 -1
- package/dist/{impl-BtIsgTGn.mjs → impl-DCqgx2-j.mjs} +2 -2
- package/dist/{impl-BtIsgTGn.mjs.map → impl-DCqgx2-j.mjs.map} +1 -1
- package/dist/{impl-CScy-GrG.mjs → impl-DJFLycxa.mjs} +2 -2
- package/dist/{impl-CScy-GrG.mjs.map → impl-DJFLycxa.mjs.map} +1 -1
- package/dist/{impl-BNDNzc2I.mjs → impl-Di_93pXw.mjs} +2 -2
- package/dist/{impl-BNDNzc2I.mjs.map → impl-Di_93pXw.mjs.map} +1 -1
- package/dist/{impl-AEjPyfhu.mjs → impl-FIRoVM7G.mjs} +2 -2
- package/dist/{impl-AEjPyfhu.mjs.map → impl-FIRoVM7G.mjs.map} +1 -1
- package/dist/{impl-BXb07jBU.mjs → impl-HoctnN3Y.mjs} +2 -2
- package/dist/{impl-BXb07jBU.mjs.map → impl-HoctnN3Y.mjs.map} +1 -1
- package/dist/{impl-BsecIND0.mjs → impl-NkVnS7sH.mjs} +2 -2
- package/dist/{impl-BsecIND0.mjs.map → impl-NkVnS7sH.mjs.map} +1 -1
- package/dist/{impl-DZicly6r.mjs → impl-Ozxwpuoj.mjs} +2 -2
- package/dist/{impl-DZicly6r.mjs.map → impl-Ozxwpuoj.mjs.map} +1 -1
- package/dist/{impl-BhnojAfL.mjs → impl-SEFLf4jX.mjs} +2 -2
- package/dist/{impl-BhnojAfL.mjs.map → impl-SEFLf4jX.mjs.map} +1 -1
- package/dist/{impl-DgG4lZ9T.mjs → impl-Wbg70K6q.mjs} +2 -2
- package/dist/{impl-DgG4lZ9T.mjs.map → impl-Wbg70K6q.mjs.map} +1 -1
- package/dist/{impl-DAkBsgQN.mjs → impl-gSRG1ELM.mjs} +2 -2
- package/dist/{impl-DAkBsgQN.mjs.map → impl-gSRG1ELM.mjs.map} +1 -1
- package/dist/{impl-Djlx-Dqj.mjs → impl-jXl2dlnG.mjs} +2 -2
- package/dist/{impl-Djlx-Dqj.mjs.map → impl-jXl2dlnG.mjs.map} +1 -1
- package/dist/index.d.mts +62 -62
- package/dist/index.mjs +1 -1
- package/dist/{markRequestDataSiloIdsCompleted-sDBo1vUD.mjs → markRequestDataSiloIdsCompleted-DTfOjCUB.mjs} +2 -2
- package/dist/{markRequestDataSiloIdsCompleted-sDBo1vUD.mjs.map → markRequestDataSiloIdsCompleted-DTfOjCUB.mjs.map} +1 -1
- package/dist/{markSilentPrivacyRequests-Cmn1fxHI.mjs → markSilentPrivacyRequests-DWyW4Ndj.mjs} +2 -2
- package/dist/{markSilentPrivacyRequests-Cmn1fxHI.mjs.map → markSilentPrivacyRequests-DWyW4Ndj.mjs.map} +1 -1
- package/dist/{notifyPrivacyRequestsAdditionalTime-CmhFE4b0.mjs → notifyPrivacyRequestsAdditionalTime-BIr4P7lx.mjs} +2 -2
- package/dist/{notifyPrivacyRequestsAdditionalTime-CmhFE4b0.mjs.map → notifyPrivacyRequestsAdditionalTime-BIr4P7lx.mjs.map} +1 -1
- package/dist/{pullChunkedCustomSiloOutstandingIdentifiers-QRET4M0x.mjs → pullChunkedCustomSiloOutstandingIdentifiers-Cb7HbDD8.mjs} +2 -2
- package/dist/{pullChunkedCustomSiloOutstandingIdentifiers-QRET4M0x.mjs.map → pullChunkedCustomSiloOutstandingIdentifiers-Cb7HbDD8.mjs.map} +1 -1
- package/dist/{pullManualEnrichmentIdentifiersToCsv-8I6PgBQc.mjs → pullManualEnrichmentIdentifiersToCsv-WvXvuTGM.mjs} +2 -2
- package/dist/{pullManualEnrichmentIdentifiersToCsv-8I6PgBQc.mjs.map → pullManualEnrichmentIdentifiersToCsv-WvXvuTGM.mjs.map} +1 -1
- package/dist/{pushCronIdentifiersFromCsv-CBb2FvPD.mjs → pushCronIdentifiersFromCsv-DJywyHYU.mjs} +2 -2
- package/dist/{pushCronIdentifiersFromCsv-CBb2FvPD.mjs.map → pushCronIdentifiersFromCsv-DJywyHYU.mjs.map} +1 -1
- package/dist/{pushManualEnrichmentIdentifiersFromCsv-DYQq7hsN.mjs → pushManualEnrichmentIdentifiersFromCsv-DT4-FUe0.mjs} +2 -2
- package/dist/{pushManualEnrichmentIdentifiersFromCsv-DYQq7hsN.mjs.map → pushManualEnrichmentIdentifiersFromCsv-DT4-FUe0.mjs.map} +1 -1
- package/dist/{removeUnverifiedRequestIdentifiers-VCbL2BXD.mjs → removeUnverifiedRequestIdentifiers-Dt5hvhtq.mjs} +2 -2
- package/dist/{removeUnverifiedRequestIdentifiers-VCbL2BXD.mjs.map → removeUnverifiedRequestIdentifiers-Dt5hvhtq.mjs.map} +1 -1
- package/dist/{retryRequestDataSilos-BCe-WGdL.mjs → retryRequestDataSilos-Ds3bF0yd.mjs} +2 -2
- package/dist/{retryRequestDataSilos-BCe-WGdL.mjs.map → retryRequestDataSilos-Ds3bF0yd.mjs.map} +1 -1
- package/dist/{skipPreflightJobs-Bc0--Bvs.mjs → skipPreflightJobs-h8H7ZEX6.mjs} +2 -2
- package/dist/{skipPreflightJobs-Bc0--Bvs.mjs.map → skipPreflightJobs-h8H7ZEX6.mjs.map} +1 -1
- package/dist/{skipRequestDataSilos-BHbAQkpb.mjs → skipRequestDataSilos-C8y5PYaZ.mjs} +2 -2
- package/dist/{skipRequestDataSilos-BHbAQkpb.mjs.map → skipRequestDataSilos-C8y5PYaZ.mjs.map} +1 -1
- package/dist/{streamPrivacyRequestsToCsv-eB3gNhol.mjs → streamPrivacyRequestsToCsv-CipyYYbS.mjs} +2 -2
- package/dist/{streamPrivacyRequestsToCsv-eB3gNhol.mjs.map → streamPrivacyRequestsToCsv-CipyYYbS.mjs.map} +1 -1
- package/dist/{updateConsentManagerVersionToLatest-D6i1Xh6o.mjs → updateConsentManagerVersionToLatest-F6ywYQg4.mjs} +2 -2
- package/dist/{updateConsentManagerVersionToLatest-D6i1Xh6o.mjs.map → updateConsentManagerVersionToLatest-F6ywYQg4.mjs.map} +1 -1
- package/dist/{uploadConsents-BTM49EbZ.mjs → uploadConsents-C6gFr33S.mjs} +2 -2
- package/dist/{uploadConsents-BTM49EbZ.mjs.map → uploadConsents-C6gFr33S.mjs.map} +1 -1
- package/dist/{uploadCookiesFromCsv-DoC9rtEF.mjs → uploadCookiesFromCsv-DydhyjYq.mjs} +2 -2
- package/dist/{uploadCookiesFromCsv-DoC9rtEF.mjs.map → uploadCookiesFromCsv-DydhyjYq.mjs.map} +1 -1
- package/dist/{uploadDataFlowsFromCsv-DL1-cAit.mjs → uploadDataFlowsFromCsv-BDs2jS3I.mjs} +2 -2
- package/dist/{uploadDataFlowsFromCsv-DL1-cAit.mjs.map → uploadDataFlowsFromCsv-BDs2jS3I.mjs.map} +1 -1
- package/dist/{uploadPrivacyRequestsFromCsv-wXm4H4FH.mjs → uploadPrivacyRequestsFromCsv-oVE4Am-C.mjs} +2 -2
- package/dist/{uploadPrivacyRequestsFromCsv-wXm4H4FH.mjs.map → uploadPrivacyRequestsFromCsv-oVE4Am-C.mjs.map} +1 -1
- package/package.json +6 -6
package/dist/{createExtraKeyHandler-BO4lu0HO.mjs.map → createExtraKeyHandler-BkfSV_aF.mjs.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createExtraKeyHandler-BO4lu0HO.mjs","names":[],"sources":["../src/lib/pooling/showCombinedLogs.ts","../src/lib/pooling/keymap.ts","../src/lib/pooling/replayFileTailToStdout.ts","../src/lib/pooling/workerIds.ts","../src/lib/pooling/installInteractiveSwitcher.ts","../src/lib/pooling/dashboardPlugin.ts","../src/lib/pooling/uiPlugins.ts","../src/lib/pooling/createExtraKeyHandler.ts"],"sourcesContent":["/* eslint-disable no-continue, no-control-regex */\nimport { readFileSync } from 'node:fs';\n\nimport type { WorkerLogPaths } from '@transcend-io/utils';\n\n/**\n * Log locations\n */\nexport type LogLocation = 'out' | 'err' | 'structured' | 'warn' | 'info';\n\n/**\n * Which logs to show in the combined output.\n * Can include 'out' (stdout), 'err' (stderr), 'structured' (\n */\nexport type WhichLogs = Array<LogLocation>;\n\n/**\n * Show combined logs from all worker processes.\n *\n * @param slotLogPaths - Map of worker IDs to their log file paths.\n * @param whichList - one or more sources to include (e.g., ['err','out'])\n * @param filterLevel - 'error', 'warn', or 'all' to filter log levels.\n */\nexport function showCombinedLogs(\n slotLogPaths: Map<number, WorkerLogPaths | undefined>,\n whichList: WhichLogs,\n filterLevel: 'error' | 'warn' | 'all',\n): void {\n process.stdout.write('\\x1b[2J\\x1b[H');\n\n const isError = (t: string): boolean =>\n /\\b(ERROR|uncaughtException|unhandledRejection)\\b/i.test(t);\n const isWarnTag = (t: string): boolean => /\\b(WARN|WARNING)\\b/i.test(t);\n\n const lines: string[] = [];\n\n for (const [, paths] of slotLogPaths) {\n if (!paths) continue;\n\n const files: Array<{\n /** Absolute file path to read from */\n path: string;\n /** Source type for this file, used for classification */\n src: LogLocation;\n }> = [];\n for (const which of whichList) {\n if (which === 'out' && paths.outPath) {\n files.push({ path: paths.outPath, src: 'out' });\n }\n if (which === 'err' && paths.errPath) {\n files.push({ path: paths.errPath, src: 'err' });\n }\n if (which === 'structured' && paths.structuredPath) {\n files.push({ path: paths.structuredPath, src: 'structured' });\n }\n if (paths.warnPath && which === 'warn') {\n files.push({ path: paths.warnPath, src: 'warn' });\n }\n if (paths.infoPath && which === 'info') {\n files.push({ path: paths.infoPath, src: 'info' });\n }\n }\n\n for (const { path, src } of files) {\n let text = '';\n try {\n text = readFileSync(path, 'utf8');\n } catch {\n continue;\n }\n\n for (const ln of text.split('\\n')) {\n if (!ln) continue;\n\n const clean = ln.replace(/\\x1B\\[[0-9;]*m/g, '');\n\n if (filterLevel === 'all') {\n lines.push(ln);\n continue;\n }\n\n if (filterLevel === 'error') {\n if (isError(clean)) lines.push(ln);\n continue;\n }\n\n // filterLevel === 'warn'\n // Accept:\n // - explicit WARN tag anywhere\n // - OR lines from stderr that are NOT explicit errors (many warn libs print to stderr)\n // - OR lines containing the word \"warning\" (common in some libs)\n if (isWarnTag(clean) || (src === 'err' && !isError(clean))) {\n lines.push(ln);\n continue;\n }\n }\n }\n }\n\n // simple time-sort; each worker often prefixes ISO timestamps\n lines.sort((a, b) => {\n const ta = a.match(/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/)?.[0] ?? '';\n const tb = b.match(/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/)?.[0] ?? '';\n return ta.localeCompare(tb);\n });\n\n process.stdout.write(`${lines.join('\\n')}\\n`);\n process.stdout.write('\\nPress Esc/Ctrl+] to return to dashboard.\\n');\n}\n/* eslint-enable no-continue, no-control-regex */\n","import type * as readline from 'node:readline';\n\n/**\n * Map a key press to an action in the interactive dashboard.\n */\nexport type Action =\n | {\n /** Indicates attaching to a session by id. */\n type: 'ATTACH';\n /** The id of the session to attach to. */\n id: number;\n }\n | {\n /** Indicates cycling through sessions. */\n type: 'CYCLE';\n /** The direction to cycle: +1 for next, -1 for previous. */\n delta: number;\n }\n | {\n /** Indicates detaching from the current session. */\n type: 'DETACH';\n }\n | {\n /** Indicates the Ctrl+C key combination was pressed. */\n type: 'CTRL_C';\n }\n | {\n /** Indicates the Ctrl+D key combination was pressed. */\n type: 'CTRL_D';\n }\n | {\n /** Indicates quitting the dashboard. */\n type: 'QUIT';\n }\n | {\n /** Forwards an unhandled key sequence. */\n type: 'FORWARD';\n /** The key sequence to forward. */\n sequence: string;\n };\n\n/**\n * Map a key press to an action in the interactive dashboard.\n *\n * @param str - The string representation of the key press.\n * @param key - The key object containing details about the key press.\n * @param mode - The current mode of the dashboard, either 'dashboard' or 'attached'.\n * @returns An Action object representing the mapped action, or null if no action is mapped.\n */\nexport function keymap(\n str: string,\n key: readline.Key,\n mode: 'dashboard' | 'attached',\n): Action | null {\n if (key.ctrl && key.name === 'c') return { type: 'CTRL_C' };\n\n if (mode === 'dashboard') {\n if (key.name && /^[0-9]$/.test(key.name)) {\n return { type: 'ATTACH', id: Number(key.name) };\n }\n if (key.name === 'tab' && !key.shift) return { type: 'CYCLE', delta: +1 };\n if (key.name === 'tab' && key.shift) return { type: 'CYCLE', delta: -1 };\n if (key.name === 'q') return { type: 'QUIT' };\n return null;\n }\n\n // attached\n if (key.name === 'escape' || (key.ctrl && key.name === ']')) {\n return { type: 'DETACH' };\n }\n if (key.ctrl && key.name === 'd') return { type: 'CTRL_D' };\n\n const sequence = key.sequence ?? str ?? '';\n return sequence ? { type: 'FORWARD', sequence } : null;\n}\n","import { createReadStream, statSync } from 'node:fs';\n\n/**\n * Replay the tail of a file to stdout.\n *\n * @param path - The absolute path to the file to read.\n * @param maxBytes - The maximum number of bytes to read from the end of the file.\n * @param write - A function to write the output to stdout.\n */\nexport async function replayFileTailToStdout(\n path: string,\n maxBytes: number,\n write: (s: string) => void,\n): Promise<void> {\n await new Promise<void>((resolve) => {\n try {\n const st = statSync(path);\n const start = Math.max(0, st.size - maxBytes);\n const stream = createReadStream(path, { start, encoding: 'utf8' });\n stream.on('data', (chunk) => write(chunk as string));\n stream.on('end', () => resolve());\n stream.on('error', () => resolve());\n } catch {\n resolve();\n }\n });\n}\n","import type { ChildProcess } from 'node:child_process';\n\n/**\n * Get the sorted list of worker IDs from a map of ChildProcess instances.\n *\n * @param m - Map of worker IDs to ChildProcess instances.\n * @returns Sorted array of worker IDs.\n */\nexport function getWorkerIds(m: Map<number, ChildProcess>): number[] {\n return [...m.keys()].sort((a, b) => a - b);\n}\n\n/**\n * Cycles through an array of numeric IDs, returning the next ID based on a delta.\n *\n * If the `current` ID is not provided or not found in the array, the first ID is used as the starting point.\n * The function then moves forward or backward in the array by `delta` positions, wrapping around if necessary.\n *\n * @param ids - Array of numeric IDs to cycle through.\n * @param current - The current ID to start cycling from. If `null` or not found, starts from the first ID.\n * @param delta - The number of positions to move forward (positive) or backward (negative) in the array.\n * @returns The next ID in the array after cycling, or `null` if the array is empty.\n */\nexport function cycleWorkers(ids: number[], current: number | null, delta: number): number | null {\n if (!ids.length) return null;\n const cur = current == null ? ids[0] : current;\n let i = ids.indexOf(cur);\n if (i === -1) i = 0;\n i = (i + delta + ids.length) % ids.length;\n return ids[i]!;\n}\n","import type { ChildProcess } from 'node:child_process';\nimport * as readline from 'node:readline';\n\nimport type { WorkerLogPaths } from '@transcend-io/utils';\n\nimport { DEBUG } from '../../constants.js';\nimport { keymap } from './keymap.js';\nimport { replayFileTailToStdout } from './replayFileTailToStdout.js';\nimport type { WhichLogs } from './showCombinedLogs.js';\nimport { cycleWorkers, getWorkerIds } from './workerIds.js';\n\n/**\n * Key action types for the interactive switcher\n */\nexport type InteractiveDashboardMode = 'dashboard' | 'attached';\n\nexport interface SwitcherPorts {\n /** Standard input stream */\n stdin: NodeJS.ReadStream;\n /** Standard output stream */\n stdout: NodeJS.WriteStream;\n /** Standard error stream */\n stderr: NodeJS.WriteStream;\n}\n\n/**\n * Install an interactive switcher for managing worker processes.\n *\n * @param opts - Options for the switcher\n * @returns A cleanup function to remove the switcher\n */\nexport function installInteractiveSwitcher(opts: {\n /** Registry of live workers by id */\n workers: Map<number, ChildProcess>;\n /** Hooks */\n onAttach?: (id: number) => void;\n /** Optional detach handler */\n onDetach?: () => void;\n /** Optional Ctrl+C handler for parent graceful shutdown in dashboard */\n onCtrlC?: () => void; // parent graceful shutdown in dashboard\n /** Provide log paths so we can replay the tail on attach */\n getLogPaths?: (id: number) => WorkerLogPaths | undefined;\n /** How many bytes to replay from the end of each file (default 200 KB) */\n replayBytes?: number;\n /** Which logs to replay first (default ['out','err']) */\n replayWhich?: WhichLogs;\n /** Print a small banner/clear screen before replaying (optional) */\n onEnterAttachScreen?: (id: number) => void;\n /** Optional stdio ports for testing; defaults to process stdio */\n ports?: SwitcherPorts;\n}): () => void {\n const {\n workers,\n onAttach,\n onDetach,\n onCtrlC,\n getLogPaths,\n replayBytes = 200 * 1024,\n replayWhich = ['out', 'err'],\n onEnterAttachScreen,\n ports,\n } = opts;\n\n const stdin = ports?.stdin ?? process.stdin;\n const stdout = ports?.stdout ?? process.stdout;\n const stderr = ports?.stderr ?? process.stderr;\n\n const d = (...a: unknown[]): void => {\n if (DEBUG) {\n try {\n (ports?.stderr ?? process.stderr).write(`[keys] ${a.map(String).join(' ')}\\n`);\n } catch {\n // noop\n }\n }\n };\n\n if (!stdin.isTTY) {\n // Not a TTY; return a no-op cleanup\n return () => {\n // noop\n };\n }\n\n readline.emitKeypressEvents(stdin);\n stdin.setRawMode?.(true);\n\n let mode: InteractiveDashboardMode = 'dashboard';\n let focus: number | null = null;\n\n // live mirroring handlers while attached\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let outHandler: ((chunk: any) => void) | null = null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let errHandler: ((chunk: any) => void) | null = null;\n\n /**\n * Cycle through worker IDs, wrapping around.\n *\n * @param id - The current worker ID to start cycling from.\n * @returns The next worker ID after cycling, or null if no workers are available.\n */\n async function replayLogs(id: number): Promise<void> {\n if (!getLogPaths) return;\n const paths = getLogPaths(id);\n if (!paths) return;\n\n const toReplay: string[] = [];\n for (const which of replayWhich) {\n if (which === 'out') toReplay.push(paths.outPath);\n if (which === 'err') toReplay.push(paths.errPath);\n if (which === 'structured') toReplay.push(paths.structuredPath);\n }\n\n if (toReplay.length) {\n stdout.write('\\n------------ replay ------------\\n');\n for (const p of toReplay) {\n stdout.write(`\\n--- ${p} (last ~${Math.floor(replayBytes / 1024)}KB) ---\\n`);\n await replayFileTailToStdout(p, replayBytes, (s) => stdout.write(s));\n }\n stdout.write('\\n--------------------------------\\n\\n');\n }\n }\n\n const attach = async (id: number): Promise<void> => {\n d('attach()', `id=${id}`); // at function entry\n\n const w = workers.get(id);\n if (!w) return;\n\n // Detach any previous focus\n if (mode === 'attached') detach();\n\n mode = 'attached';\n focus = id;\n\n // UX: clear + banner\n onEnterAttachScreen?.(id);\n\n onAttach?.(id); // prints “Attached to worker …” and clears\n await replayLogs(id); // now the tail stays visible\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n outHandler = (chunk: any) => stdout.write(chunk);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n errHandler = (chunk: any) => stderr.write(chunk);\n w.stdout?.on('data', outHandler);\n w.stderr?.on('data', errHandler);\n\n // auto-detach if child exits\n const onExit = (): void => {\n if (focus === id) detach();\n };\n w.once('exit', onExit);\n };\n\n const detach = (): void => {\n d('detach()', `id=${focus}`); // at function entry\n\n if (focus == null) return;\n const id = focus;\n const w = workers.get(id);\n if (w) {\n if (outHandler) w.stdout?.off('data', outHandler);\n if (errHandler) w.stderr?.off('data', errHandler);\n }\n outHandler = null;\n errHandler = null;\n focus = null;\n mode = 'dashboard';\n onDetach?.();\n };\n\n const onKey = (str: string, key: readline.Key): void => {\n d(\n 'keypress',\n JSON.stringify({\n str,\n name: key.name,\n seq: key.sequence,\n ctrl: key.ctrl,\n meta: key.meta,\n shift: key.shift,\n mode,\n }),\n );\n const act = keymap(str, key, mode);\n d('mapped', JSON.stringify(act));\n\n if (!act) return;\n\n // eslint-disable-next-line default-case\n switch (act.type) {\n case 'CTRL_C': {\n d('CTRL_C');\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.kill('SIGINT');\n } catch {\n // noop\n }\n // optional: auto-detach so second Ctrl+C exits parent\n detach();\n return;\n }\n onCtrlC?.();\n return;\n }\n\n case 'ATTACH': {\n d('ATTACH', `id=${act.id}`, `has=${workers.has(act.id)}`);\n\n if (mode !== 'dashboard') return;\n // eslint-disable-next-line no-void\n if (workers.has(act.id)) void attach(act.id);\n return;\n }\n\n case 'CYCLE': {\n d('CYCLE', `delta=${act.delta}`);\n if (mode !== 'dashboard') return;\n const next = cycleWorkers(getWorkerIds(workers), focus, act.delta);\n // eslint-disable-next-line no-void\n if (next != null) void attach(next);\n return;\n }\n\n case 'QUIT': {\n if (mode !== 'dashboard') return;\n onCtrlC?.();\n return;\n }\n\n case 'DETACH': {\n d('DETACH');\n if (mode === 'attached') detach();\n return;\n }\n\n case 'CTRL_D': {\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.stdin?.end();\n } catch {\n // noop\n }\n }\n return;\n }\n\n case 'FORWARD': {\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.stdin?.write(act.sequence);\n } catch {\n // noop\n }\n }\n }\n }\n };\n\n // Raw bytes fallback (usually not hit because keypress handles it)\n const onData = (chunk: Buffer): void => {\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.stdin?.write(chunk);\n } catch {\n // noop\n }\n }\n };\n\n const cleanup = (): void => {\n stdin.off('keypress', onKey);\n stdin.off('data', onData);\n stdin.setRawMode?.(false);\n stdout.write('\\x1b[?25h');\n };\n\n stdin.on('keypress', onKey);\n stdin.on('data', onData);\n\n return cleanup;\n}\n","// lib/pooling/dashboardPlugin.ts\nimport * as readline from 'node:readline';\n\nimport type { ObjByString } from '@transcend-io/type-utils';\nimport type { SlotState } from '@transcend-io/utils';\nimport colors from 'colors';\n\n/**\n * A dashboard plugin defines how to render the worker pool UI.\n * Commands can supply a plugin to customize:\n * - The header block (summary stats, title, etc.)\n * - Per-worker rows (one line per worker slot)\n * - Optional extras (artifact exports, breakdowns, footers)\n *\n * @template TTotals - The shape of the aggregate totals object maintained by the command.\n */\nexport interface DashboardPlugin<TTotals, TSlotState extends ObjByString> {\n /**\n * Render the header block of the dashboard.\n *\n * @param ctx - Context with pool/worker state, totals, and metadata.\n * @returns An array of strings, each representing one line in the header.\n */\n renderHeader: (ctx: CommonCtx<TTotals, TSlotState>) => string[];\n\n /**\n * Render per-worker rows, usually one line per worker slot.\n *\n * @param ctx - Context with pool/worker state, totals, and metadata.\n * @returns An array of strings, each representing one row in the workers section.\n */\n renderWorkers: (ctx: CommonCtx<TTotals, TSlotState>) => string[];\n\n /**\n * Render any optional extra blocks that appear after the worker rows.\n * Useful for printing export paths, aggregated metrics, breakdowns, etc.\n *\n * @param ctx - Context with pool/worker state, totals, and metadata.\n * @returns An array of strings, each representing one additional line.\n */\n renderExtras?: (ctx: CommonCtx<TTotals, TSlotState>) => string[];\n}\n\n/**\n * Shared context object passed into all render methods of a {@link DashboardPlugin}.\n *\n * @template TTotals - The shape of the aggregate totals object maintained by the command.\n */\nexport type CommonCtx<TTotals, TSlotState extends ObjByString> = {\n /** Human-readable title for the dashboard (e.g., \"Parallel uploader\"). */\n title: string;\n\n /** Number of worker processes spawned in the pool. */\n poolSize: number;\n\n /** Logical CPU count, included for informational display. */\n cpuCount: number;\n\n /** Total number of \"files\" or logical units the command expects to process. */\n filesTotal: number;\n\n /** Count of successfully completed files/tasks. */\n filesCompleted: number;\n\n /** Count of failed files/tasks. */\n filesFailed: number;\n\n /**\n * State of each worker slot, keyed by worker id.\n * Includes busy flag, file label, start time, last log badge, and progress.\n */\n workerState: Map<number, SlotState<TSlotState>>;\n\n /**\n * Aggregate totals maintained by the command’s hook logic.\n * Domain-specific metrics (e.g., rows uploaded, bytes processed) can be surfaced here.\n */\n totals: TTotals;\n\n /**\n * Throughput metrics tracked by the runner:\n * - successSoFar: convenience alias for completed count\n * - r10s: completions/sec averaged over last 10s\n * - r60s: completions/sec averaged over last 60s\n */\n throughput: {\n /** Cumulative count of successful completions so far. */\n successSoFar: number;\n /** Recent file-level throughput rate over the last 10 seconds. */\n r10s: number;\n /** Recent file-level throughput rate over the last 60 seconds. */\n r60s: number;\n /** Recent job/record-level throughput rate over the last 10 seconds. */\n jobsR10s: number;\n /** Recent job/record-level throughput rate over the last 60 seconds. */\n jobsR60s: number;\n };\n\n /** True when the pool has fully drained and all workers have exited. */\n final: boolean;\n\n /**\n * Optional export status payload provided by the command.\n * Useful for rendering artifact paths or \"latest export\" summaries.\n */\n exportStatus?: Record<string, unknown>;\n};\n\n/** The most recently rendered frame, cached to suppress flicker from duplicate renders. */\nlet lastFrame = '';\n\n/**\n * Generate the hotkeys hint string that appears at the bottom of the dashboard.\n *\n * @param poolSize - The number of worker slots in the pool.\n * @param final - Whether the run has completed.\n * @returns A dimmed string listing the supported hotkeys for attach/detach/quit.\n */\nexport const hotkeysHint = (poolSize: number, final: boolean): string => {\n const maxDigit = Math.min(poolSize - 1, 9);\n const digitRange = poolSize <= 1 ? '0' : `0-${maxDigit}`;\n const extra = poolSize > 10 ? ' (Tab/Shift+Tab for ≥10)' : '';\n return final\n ? colors.dim(\n 'Run complete — digits to view logs • Tab/Shift+Tab cycle • Esc/Ctrl+] detach • q to quit',\n )\n : colors.dim(\n `Hotkeys: [${digitRange}] attach${extra} • e=errors • w=warnings • i=info • l=logs • ` +\n 'Tab/Shift+Tab • Esc/Ctrl+] detach • Ctrl+C exit',\n );\n};\n\n/**\n * Render the dashboard using a supplied {@link DashboardPlugin}.\n *\n * The frame is composed of:\n * - Header lines\n * - A blank separator\n * - Worker rows\n * - A blank separator\n * - Hotkeys hint\n * - Optional extras (if plugin supplies them)\n *\n * Optimizations:\n * - Suppresses re-renders if the frame is identical to the previous frame (flicker-free).\n * - Hides the terminal cursor during live updates, restoring it when final.\n *\n * @param ctx - Shared context containing pool state, worker state, totals, throughput, etc.\n * @param plugin - The plugin that defines how to render the header, workers, and optional extras.\n * @param viewerMode - If true, renders in viewer mode (no ability to switch between files).\n */\nexport function dashboardPlugin<TTotals, TSlotState extends ObjByString>(\n ctx: CommonCtx<TTotals, TSlotState>,\n plugin: DashboardPlugin<TTotals, TSlotState>,\n viewerMode = false,\n): void {\n const frame = [\n ...plugin.renderHeader(ctx),\n '',\n ...plugin.renderWorkers(ctx),\n ...(viewerMode ? [] : ['', hotkeysHint(ctx.poolSize, ctx.final)]),\n ...(plugin.renderExtras ? [''].concat(plugin.renderExtras(ctx)) : []),\n ].join('\\n');\n\n // Skip duplicate renders during live runs to avoid flicker.\n if (!ctx.final && frame === lastFrame) return;\n lastFrame = frame;\n\n if (!ctx.final) {\n // Hide cursor and repaint in place\n process.stdout.write('\\x1b[?25l');\n readline.cursorTo(process.stdout, 0, 0);\n readline.clearScreenDown(process.stdout);\n } else {\n // Restore cursor on final render\n process.stdout.write('\\x1b[?25h');\n }\n process.stdout.write(`${frame}\\n`);\n}\n","import { basename } from 'node:path';\n\nimport type { ObjByString } from '@transcend-io/type-utils';\nimport colors from 'colors';\n\nimport type { CommonCtx } from './dashboardPlugin.js';\n\n/**\n * Progress snapshot for a worker slot in the chunk-csv command.\n */\nexport type ChunkSlotProgress = {\n /** Absolute path of the file being processed by this worker. */\n filePath?: string;\n /** Number of rows processed so far in this file. */\n processed?: number;\n /** Optional total number of rows in the file (if known). */\n total?: number;\n};\n\n/**\n * Format a number safely for display.\n *\n * @param n - The number to format (or `undefined`).\n * @returns A localized string representation, or \"0\".\n */\nexport function fmtNum(n: number | undefined): string {\n return typeof n === 'number' ? n.toLocaleString() : '0';\n}\n\n/**\n * Draw a horizontal bar of length `width` filled to `pct` percent.\n *\n * @param pct - Percentage 0..100.\n * @param width - Number of characters in the bar.\n * @returns A string like \"████░░░░\".\n */\nexport function pctBar(pct: number, width = 40): string {\n const clamped = Math.max(0, Math.min(100, Math.floor(pct)));\n const filled = Math.floor((clamped / 100) * width);\n return '█'.repeat(filled) + '░'.repeat(width - filled);\n}\n\n/**\n * Compute pool-wide progress values needed by headers.\n *\n * @param ctx - Dashboard context containing pool state, worker state, totals, etc.\n * @returns An object with `done`, `inProgress`, and `pct` properties.\n */\nexport function poolProgress<TTotals, TSlot extends ObjByString>(\n ctx: CommonCtx<TTotals, TSlot>,\n): {\n /** Count of successfully completed files/tasks. */\n done: number;\n /** Count of currently in-progress files/tasks. */\n inProgress: number;\n /** Percentage of completion (0-100). */\n pct: number;\n} {\n const inProgress = [...ctx.workerState.values()].filter((s) => s.busy).length;\n const done = ctx.filesCompleted + ctx.filesFailed;\n const pct = ctx.filesTotal === 0 ? 100 : Math.floor((done / Math.max(1, ctx.filesTotal)) * 100);\n return { done, inProgress, pct };\n}\n\n/**\n * Compose the common header lines (title, pool stats, progress bar, throughput).\n *\n * @param ctx - Dashboard context.\n * @param extraLines - Optional extra lines (e.g., totals block).\n * @returns Header lines.\n */\nexport function makeHeader<TTotals, TSlot extends ObjByString>(\n ctx: CommonCtx<TTotals, TSlot>,\n extraLines: string[] = [],\n): string[] {\n const { title, poolSize, cpuCount, filesTotal, filesCompleted, filesFailed, throughput } = ctx;\n const { inProgress, pct } = poolProgress(ctx);\n\n const lines: string[] = [\n `${colors.bold(title)} — ${poolSize} workers ${colors.dim(`(CPU avail: ${cpuCount})`)}`,\n `${colors.dim('Files')} ${fmtNum(filesTotal)} ${colors.dim(\n 'Completed',\n )} ${fmtNum(filesCompleted)} ${colors.dim('Failed')} ${\n filesFailed ? colors.red(fmtNum(filesFailed)) : fmtNum(filesFailed)\n } ${colors.dim('In-flight')} ${fmtNum(inProgress)}`,\n `[${pctBar(pct)}] ${pct}%`,\n ];\n\n if (throughput) {\n const jobsActive = throughput.jobsR10s > 0 || throughput.jobsR60s > 0;\n const perHour10 = Math.round(\n (jobsActive ? throughput.jobsR10s : throughput.r10s) * 3600,\n ).toLocaleString();\n const perHour60 = Math.round(\n (jobsActive ? throughput.jobsR60s : throughput.r60s) * 3600,\n ).toLocaleString();\n const unit = jobsActive ? 'rec' : 'files';\n const suffix =\n ctx.throughput?.successSoFar != null\n ? ` Newly uploaded: ${fmtNum(ctx.throughput.successSoFar)}`\n : '';\n lines.push(\n colors.cyan(`Throughput: ${perHour10} ${unit}/hr (1h: ${perHour60} ${unit}/hr)${suffix}`),\n );\n }\n\n return extraLines.length ? lines.concat(extraLines) : lines;\n}\n\n/**\n * Render per-worker rows with a compact progress bar and status badge.\n *\n * @param ctx - Dashboard context (slot progress type must have processed/total?).\n * @param getFileLabel - Optional: override how the filename is shown.\n * @returns Array of strings, each representing one worker row.\n */\nexport function makeWorkerRows<TTotals, TSlot extends Omit<ChunkSlotProgress, 'filePath'>>(\n ctx: CommonCtx<TTotals, TSlot>,\n getFileLabel: (file: string | null | undefined) => string = (file) =>\n file ? basename(file) : '-',\n): string[] {\n const miniWidth = 18;\n\n return [...ctx.workerState.entries()].map(([id, s]) => {\n const badge =\n s.lastLevel === 'error'\n ? colors.red('ERROR ')\n : s.lastLevel === 'warn'\n ? colors.yellow('WARN ')\n : s.busy\n ? colors.green('WORKING')\n : colors.dim('IDLE ');\n\n const fname = getFileLabel(s.file);\n const elapsed = s.startedAt ? `${Math.floor((Date.now() - s.startedAt) / 1000)}s` : '-';\n\n const processed = s.progress?.processed ?? 0;\n const total = s.progress?.total ?? 0;\n const pctw = total > 0 ? Math.floor((processed / total) * 100) : 0;\n const mini = total > 0 ? pctBar(pctw, miniWidth) : ' '.repeat(miniWidth);\n const miniTxt =\n total > 0\n ? `${processed.toLocaleString()}/${total.toLocaleString()} (${pctw}%)`\n : colors.dim('—');\n\n return ` [w${id}] ${badge} | ${fname} | ${elapsed} | [${mini}] ${miniTxt}`;\n });\n}\n","import type { ExportStatusMap, SlotPaths } from '@transcend-io/utils';\n\nimport { showCombinedLogs, type LogLocation } from './showCombinedLogs.js';\n\n/** Severity filter applied by the viewer. */\ntype ViewLevel = 'error' | 'warn' | 'all';\n\n/**\n * Options for {@link createExtraKeyHandler}.\n */\nexport type CreateExtraKeyHandlerOpts = {\n /**\n * Per-slot log file paths maintained by the runner; used to stream or export logs.\n */\n logsBySlot: SlotPaths;\n\n /**\n * Request an immediate dashboard repaint (e.g., after updating export status).\n */\n repaint: () => void;\n\n /**\n * Pause/unpause dashboard repainting. The handler pauses while a viewer is open\n * to prevent the dashboard from overwriting the viewer output, then resumes on exit.\n */\n setPaused: (p: boolean) => void;\n\n /**\n * Optional export manager to enable uppercase export keys:\n * - `E` (errors) • `W` (warnings) • `I` (info) • `A` (all)\n *\n * Provide this only if your command supports writing combined log files.\n */\n exportMgr?: {\n /** Destination directory for exported artifacts. */\n exportsDir: string;\n /**\n * Write a combined log file for the selected severity and return the absolute path.\n *\n * @param logs - Log paths to combine.\n * @param which - Severity selection.\n * @returns Absolute path to the written file.\n */\n exportCombinedLogs: (logs: SlotPaths, which: 'error' | 'warn' | 'info' | 'all') => string;\n };\n\n /**\n * Optional “Exports” status map. If provided, the handler updates timestamps\n * when exports are written so your dashboard panel can reflect “last saved” times.\n */\n exportStatus?: ExportStatusMap;\n\n /**\n * Optional custom key bindings for command-specific actions.\n * Each handler receives helpers to print messages and to update the exports panel.\n *\n * Example:\n * ```ts\n * custom: {\n * F: async ({ say, noteExport }) => {\n * const p = await writeFailingUpdatesCsv(...);\n * say(`Wrote failing updates to: ${p}`);\n * noteExport('failuresCsv', p);\n * }\n * }\n * ```\n */\n custom?: Record<\n string,\n (ctx: {\n /** Update {@link exportStatus} (if present) and repaint the dashboard. */\n noteExport: (slot: keyof ExportStatusMap, absPath: string) => void;\n /** Print a line to stdout, automatically newline-terminated. */\n say: (s: string) => void;\n }) => void | Promise<void>\n >;\n};\n\n/**\n * Create a keypress handler for interactive viewers/exports.\n * Shared handler for \"extra\" keyboard shortcuts used by the interactive dashboard.\n *\n * It wires:\n * - **Viewers (lowercase):** `e` (errors), `w` (warnings), `i` (info), `l` (all)\n * - **Exports (uppercase, optional):** `E` (errors), `W` (warnings), `I` (info), `A` (all)\n * - **Dismiss:** `Esc` or `Ctrl+]` exits a viewer and returns to the dashboard\n * - **Custom keys (optional):** Provide a `custom` map to handle command-specific bindings\n *\n * Usage (inside `runPool({... extraKeyHandler })`):\n * ```ts\n * extraKeyHandler: ({ logsBySlot, repaint, setPaused }) =>\n * createExtraKeyHandler({ logsBySlot, repaint, setPaused })\n * ```\n *\n * If you also want export hotkeys + an \"Exports\" panel:\n * ```ts\n * extraKeyHandler: ({ logsBySlot, repaint, setPaused }) =>\n * createExtraKeyHandler({\n * logsBySlot, repaint, setPaused,\n * exportMgr, // enables E/W/I/A\n * exportStatus, // keeps panel timestamps up to date\n * custom: { // optional, e.g. 'F' to export a CSV\n * F: async ({ say, noteExport }) => { ... }\n * }\n * })\n * ```\n *\n * @param opts - Configuration for viewers, exports, and custom keys.\n * @returns A `(buf: Buffer) => void` handler suitable for `process.stdin.on('data', ...)`.\n */\nexport function createExtraKeyHandler(opts: CreateExtraKeyHandlerOpts): (buf: Buffer) => void {\n const { logsBySlot, repaint, setPaused, exportMgr, exportStatus, custom } = opts;\n\n const say = (s: string): void => {\n process.stdout.write(`${s}\\n`);\n };\n\n /**\n * Record that an export was written and trigger a repaint so the dashboard’s\n * \"Exports\" panel shows the updated timestamp/path.\n *\n * @param slot - Slot name in {@link ExportStatusMap} (e.g., \"error\", \"warn\", etc.).\n * @param p - Absolute path to the exported file.\n */\n const noteExport = (slot: keyof ExportStatusMap, p: string): void => {\n const now = Date.now();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const cur: any = exportStatus?.[slot] ?? { path: p };\n if (exportStatus) {\n exportStatus[slot] = {\n path: p || cur.path,\n savedAt: now,\n exported: true,\n };\n repaint();\n }\n };\n\n let viewing = false; // optional guard to prevent stacking viewers\n\n /**\n * Show an inline combined log viewer for the selected sources/level.\n * Pauses dashboard repaint to keep the viewer visible until the user exits.\n *\n * @param sources - Log sources to include (e.g., \"err\", \"warn\", \"info\").\n * @param level - Severity level to filter by (e.g., \"error\", \"warn\", \"all\").\n */\n const view = (sources: LogLocation[], level: ViewLevel): void => {\n if (viewing) return;\n viewing = true;\n setPaused(true);\n\n // optional UX: clear screen and show a hint\n process.stdout.write('\\x1b[2J\\x1b[H'); // clear+home\n process.stdout.write('Combined logs viewer (press Esc or Ctrl+] to return)\\n\\n');\n\n (async () => {\n try {\n await showCombinedLogs(logsBySlot, sources, level);\n // NOTE: do NOT unpause here; ESC will handle it.\n } catch {\n // If showCombinedLogs throws, recover and unpause\n viewing = false;\n setPaused(false);\n repaint();\n }\n })();\n };\n\n /**\n * Export combined logs (if an export manager was provided).\n *\n * @param which - Severity to export (e.g., \"error\", \"warn\", \"info\", \"all\").\n * @param label - Human-readable label for the export (e.g., \"error\", \"warn\").\n */\n const exportCombined = (which: 'error' | 'warn' | 'info' | 'all', label: string): void => {\n if (!exportMgr) return;\n try {\n const p = exportMgr.exportCombinedLogs(logsBySlot, which);\n say(`\\nWrote combined ${label} logs to: ${p}`);\n noteExport(which as keyof ExportStatusMap, p);\n } catch {\n say(`\\nFailed to write combined ${label} logs`);\n }\n };\n\n // The keypress handler the runner will attach to stdin.\n return (buf: Buffer): void => {\n const s = buf.toString('utf8');\n\n // Viewers (lowercase)\n if (s === 'e') {\n view(['err'], 'error');\n return;\n }\n if (s === 'w') {\n view(['warn', 'err'], 'warn');\n return;\n }\n if (s === 'i') {\n view(['info'], 'all');\n return;\n }\n if (s === 'l') {\n view(['out', 'err', 'structured'], 'all');\n return;\n }\n\n // Exports (uppercase) — enabled only when exportMgr is present\n if (s === 'E') {\n exportCombined('error', 'error');\n return;\n }\n if (s === 'W') {\n exportCombined('warn', 'warn');\n return;\n }\n if (s === 'I') {\n exportCombined('info', 'info');\n return;\n }\n if (s === 'A') {\n exportCombined('all', 'ALL');\n return;\n }\n\n // Command-specific bindings\n const fn = custom?.[s];\n if (fn) {\n fn({ noteExport, say });\n return;\n }\n\n // Exit a viewer (Esc / Ctrl+]) — resume dashboard\n if (s === '\\x1b' || s === '\\x1d') {\n viewing = false;\n setPaused(false);\n repaint();\n }\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createExtraKeyHandler-BkfSV_aF.mjs","names":[],"sources":["../src/lib/pooling/showCombinedLogs.ts","../src/lib/pooling/keymap.ts","../src/lib/pooling/replayFileTailToStdout.ts","../src/lib/pooling/workerIds.ts","../src/lib/pooling/installInteractiveSwitcher.ts","../src/lib/pooling/dashboardPlugin.ts","../src/lib/pooling/uiPlugins.ts","../src/lib/pooling/createExtraKeyHandler.ts"],"sourcesContent":["/* eslint-disable no-continue, no-control-regex */\nimport { readFileSync } from 'node:fs';\n\nimport type { WorkerLogPaths } from '@transcend-io/utils';\n\n/**\n * Log locations\n */\nexport type LogLocation = 'out' | 'err' | 'structured' | 'warn' | 'info';\n\n/**\n * Which logs to show in the combined output.\n * Can include 'out' (stdout), 'err' (stderr), 'structured' (\n */\nexport type WhichLogs = Array<LogLocation>;\n\n/**\n * Show combined logs from all worker processes.\n *\n * @param slotLogPaths - Map of worker IDs to their log file paths.\n * @param whichList - one or more sources to include (e.g., ['err','out'])\n * @param filterLevel - 'error', 'warn', or 'all' to filter log levels.\n */\nexport function showCombinedLogs(\n slotLogPaths: Map<number, WorkerLogPaths | undefined>,\n whichList: WhichLogs,\n filterLevel: 'error' | 'warn' | 'all',\n): void {\n process.stdout.write('\\x1b[2J\\x1b[H');\n\n const isError = (t: string): boolean =>\n /\\b(ERROR|uncaughtException|unhandledRejection)\\b/i.test(t);\n const isWarnTag = (t: string): boolean => /\\b(WARN|WARNING)\\b/i.test(t);\n\n const lines: string[] = [];\n\n for (const [, paths] of slotLogPaths) {\n if (!paths) continue;\n\n const files: Array<{\n /** Absolute file path to read from */\n path: string;\n /** Source type for this file, used for classification */\n src: LogLocation;\n }> = [];\n for (const which of whichList) {\n if (which === 'out' && paths.outPath) {\n files.push({ path: paths.outPath, src: 'out' });\n }\n if (which === 'err' && paths.errPath) {\n files.push({ path: paths.errPath, src: 'err' });\n }\n if (which === 'structured' && paths.structuredPath) {\n files.push({ path: paths.structuredPath, src: 'structured' });\n }\n if (paths.warnPath && which === 'warn') {\n files.push({ path: paths.warnPath, src: 'warn' });\n }\n if (paths.infoPath && which === 'info') {\n files.push({ path: paths.infoPath, src: 'info' });\n }\n }\n\n for (const { path, src } of files) {\n let text = '';\n try {\n text = readFileSync(path, 'utf8');\n } catch {\n continue;\n }\n\n for (const ln of text.split('\\n')) {\n if (!ln) continue;\n\n const clean = ln.replace(/\\x1B\\[[0-9;]*m/g, '');\n\n if (filterLevel === 'all') {\n lines.push(ln);\n continue;\n }\n\n if (filterLevel === 'error') {\n if (isError(clean)) lines.push(ln);\n continue;\n }\n\n // filterLevel === 'warn'\n // Accept:\n // - explicit WARN tag anywhere\n // - OR lines from stderr that are NOT explicit errors (many warn libs print to stderr)\n // - OR lines containing the word \"warning\" (common in some libs)\n if (isWarnTag(clean) || (src === 'err' && !isError(clean))) {\n lines.push(ln);\n continue;\n }\n }\n }\n }\n\n // simple time-sort; each worker often prefixes ISO timestamps\n lines.sort((a, b) => {\n const ta = a.match(/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/)?.[0] ?? '';\n const tb = b.match(/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/)?.[0] ?? '';\n return ta.localeCompare(tb);\n });\n\n process.stdout.write(`${lines.join('\\n')}\\n`);\n process.stdout.write('\\nPress Esc/Ctrl+] to return to dashboard.\\n');\n}\n/* eslint-enable no-continue, no-control-regex */\n","import type * as readline from 'node:readline';\n\n/**\n * Map a key press to an action in the interactive dashboard.\n */\nexport type Action =\n | {\n /** Indicates attaching to a session by id. */\n type: 'ATTACH';\n /** The id of the session to attach to. */\n id: number;\n }\n | {\n /** Indicates cycling through sessions. */\n type: 'CYCLE';\n /** The direction to cycle: +1 for next, -1 for previous. */\n delta: number;\n }\n | {\n /** Indicates detaching from the current session. */\n type: 'DETACH';\n }\n | {\n /** Indicates the Ctrl+C key combination was pressed. */\n type: 'CTRL_C';\n }\n | {\n /** Indicates the Ctrl+D key combination was pressed. */\n type: 'CTRL_D';\n }\n | {\n /** Indicates quitting the dashboard. */\n type: 'QUIT';\n }\n | {\n /** Forwards an unhandled key sequence. */\n type: 'FORWARD';\n /** The key sequence to forward. */\n sequence: string;\n };\n\n/**\n * Map a key press to an action in the interactive dashboard.\n *\n * @param str - The string representation of the key press.\n * @param key - The key object containing details about the key press.\n * @param mode - The current mode of the dashboard, either 'dashboard' or 'attached'.\n * @returns An Action object representing the mapped action, or null if no action is mapped.\n */\nexport function keymap(\n str: string,\n key: readline.Key,\n mode: 'dashboard' | 'attached',\n): Action | null {\n if (key.ctrl && key.name === 'c') return { type: 'CTRL_C' };\n\n if (mode === 'dashboard') {\n if (key.name && /^[0-9]$/.test(key.name)) {\n return { type: 'ATTACH', id: Number(key.name) };\n }\n if (key.name === 'tab' && !key.shift) return { type: 'CYCLE', delta: +1 };\n if (key.name === 'tab' && key.shift) return { type: 'CYCLE', delta: -1 };\n if (key.name === 'q') return { type: 'QUIT' };\n return null;\n }\n\n // attached\n if (key.name === 'escape' || (key.ctrl && key.name === ']')) {\n return { type: 'DETACH' };\n }\n if (key.ctrl && key.name === 'd') return { type: 'CTRL_D' };\n\n const sequence = key.sequence ?? str ?? '';\n return sequence ? { type: 'FORWARD', sequence } : null;\n}\n","import { createReadStream, statSync } from 'node:fs';\n\n/**\n * Replay the tail of a file to stdout.\n *\n * @param path - The absolute path to the file to read.\n * @param maxBytes - The maximum number of bytes to read from the end of the file.\n * @param write - A function to write the output to stdout.\n */\nexport async function replayFileTailToStdout(\n path: string,\n maxBytes: number,\n write: (s: string) => void,\n): Promise<void> {\n await new Promise<void>((resolve) => {\n try {\n const st = statSync(path);\n const start = Math.max(0, st.size - maxBytes);\n const stream = createReadStream(path, { start, encoding: 'utf8' });\n stream.on('data', (chunk) => write(chunk as string));\n stream.on('end', () => resolve());\n stream.on('error', () => resolve());\n } catch {\n resolve();\n }\n });\n}\n","import type { ChildProcess } from 'node:child_process';\n\n/**\n * Get the sorted list of worker IDs from a map of ChildProcess instances.\n *\n * @param m - Map of worker IDs to ChildProcess instances.\n * @returns Sorted array of worker IDs.\n */\nexport function getWorkerIds(m: Map<number, ChildProcess>): number[] {\n return [...m.keys()].sort((a, b) => a - b);\n}\n\n/**\n * Cycles through an array of numeric IDs, returning the next ID based on a delta.\n *\n * If the `current` ID is not provided or not found in the array, the first ID is used as the starting point.\n * The function then moves forward or backward in the array by `delta` positions, wrapping around if necessary.\n *\n * @param ids - Array of numeric IDs to cycle through.\n * @param current - The current ID to start cycling from. If `null` or not found, starts from the first ID.\n * @param delta - The number of positions to move forward (positive) or backward (negative) in the array.\n * @returns The next ID in the array after cycling, or `null` if the array is empty.\n */\nexport function cycleWorkers(ids: number[], current: number | null, delta: number): number | null {\n if (!ids.length) return null;\n const cur = current == null ? ids[0] : current;\n let i = ids.indexOf(cur);\n if (i === -1) i = 0;\n i = (i + delta + ids.length) % ids.length;\n return ids[i]!;\n}\n","import type { ChildProcess } from 'node:child_process';\nimport * as readline from 'node:readline';\n\nimport type { WorkerLogPaths } from '@transcend-io/utils';\n\nimport { DEBUG } from '../../constants.js';\nimport { keymap } from './keymap.js';\nimport { replayFileTailToStdout } from './replayFileTailToStdout.js';\nimport type { WhichLogs } from './showCombinedLogs.js';\nimport { cycleWorkers, getWorkerIds } from './workerIds.js';\n\n/**\n * Key action types for the interactive switcher\n */\nexport type InteractiveDashboardMode = 'dashboard' | 'attached';\n\nexport interface SwitcherPorts {\n /** Standard input stream */\n stdin: NodeJS.ReadStream;\n /** Standard output stream */\n stdout: NodeJS.WriteStream;\n /** Standard error stream */\n stderr: NodeJS.WriteStream;\n}\n\n/**\n * Install an interactive switcher for managing worker processes.\n *\n * @param opts - Options for the switcher\n * @returns A cleanup function to remove the switcher\n */\nexport function installInteractiveSwitcher(opts: {\n /** Registry of live workers by id */\n workers: Map<number, ChildProcess>;\n /** Hooks */\n onAttach?: (id: number) => void;\n /** Optional detach handler */\n onDetach?: () => void;\n /** Optional Ctrl+C handler for parent graceful shutdown in dashboard */\n onCtrlC?: () => void; // parent graceful shutdown in dashboard\n /** Provide log paths so we can replay the tail on attach */\n getLogPaths?: (id: number) => WorkerLogPaths | undefined;\n /** How many bytes to replay from the end of each file (default 200 KB) */\n replayBytes?: number;\n /** Which logs to replay first (default ['out','err']) */\n replayWhich?: WhichLogs;\n /** Print a small banner/clear screen before replaying (optional) */\n onEnterAttachScreen?: (id: number) => void;\n /** Optional stdio ports for testing; defaults to process stdio */\n ports?: SwitcherPorts;\n}): () => void {\n const {\n workers,\n onAttach,\n onDetach,\n onCtrlC,\n getLogPaths,\n replayBytes = 200 * 1024,\n replayWhich = ['out', 'err'],\n onEnterAttachScreen,\n ports,\n } = opts;\n\n const stdin = ports?.stdin ?? process.stdin;\n const stdout = ports?.stdout ?? process.stdout;\n const stderr = ports?.stderr ?? process.stderr;\n\n const d = (...a: unknown[]): void => {\n if (DEBUG) {\n try {\n (ports?.stderr ?? process.stderr).write(`[keys] ${a.map(String).join(' ')}\\n`);\n } catch {\n // noop\n }\n }\n };\n\n if (!stdin.isTTY) {\n // Not a TTY; return a no-op cleanup\n return () => {\n // noop\n };\n }\n\n readline.emitKeypressEvents(stdin);\n stdin.setRawMode?.(true);\n\n let mode: InteractiveDashboardMode = 'dashboard';\n let focus: number | null = null;\n\n // live mirroring handlers while attached\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let outHandler: ((chunk: any) => void) | null = null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let errHandler: ((chunk: any) => void) | null = null;\n\n /**\n * Cycle through worker IDs, wrapping around.\n *\n * @param id - The current worker ID to start cycling from.\n * @returns The next worker ID after cycling, or null if no workers are available.\n */\n async function replayLogs(id: number): Promise<void> {\n if (!getLogPaths) return;\n const paths = getLogPaths(id);\n if (!paths) return;\n\n const toReplay: string[] = [];\n for (const which of replayWhich) {\n if (which === 'out') toReplay.push(paths.outPath);\n if (which === 'err') toReplay.push(paths.errPath);\n if (which === 'structured') toReplay.push(paths.structuredPath);\n }\n\n if (toReplay.length) {\n stdout.write('\\n------------ replay ------------\\n');\n for (const p of toReplay) {\n stdout.write(`\\n--- ${p} (last ~${Math.floor(replayBytes / 1024)}KB) ---\\n`);\n await replayFileTailToStdout(p, replayBytes, (s) => stdout.write(s));\n }\n stdout.write('\\n--------------------------------\\n\\n');\n }\n }\n\n const attach = async (id: number): Promise<void> => {\n d('attach()', `id=${id}`); // at function entry\n\n const w = workers.get(id);\n if (!w) return;\n\n // Detach any previous focus\n if (mode === 'attached') detach();\n\n mode = 'attached';\n focus = id;\n\n // UX: clear + banner\n onEnterAttachScreen?.(id);\n\n onAttach?.(id); // prints “Attached to worker …” and clears\n await replayLogs(id); // now the tail stays visible\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n outHandler = (chunk: any) => stdout.write(chunk);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n errHandler = (chunk: any) => stderr.write(chunk);\n w.stdout?.on('data', outHandler);\n w.stderr?.on('data', errHandler);\n\n // auto-detach if child exits\n const onExit = (): void => {\n if (focus === id) detach();\n };\n w.once('exit', onExit);\n };\n\n const detach = (): void => {\n d('detach()', `id=${focus}`); // at function entry\n\n if (focus == null) return;\n const id = focus;\n const w = workers.get(id);\n if (w) {\n if (outHandler) w.stdout?.off('data', outHandler);\n if (errHandler) w.stderr?.off('data', errHandler);\n }\n outHandler = null;\n errHandler = null;\n focus = null;\n mode = 'dashboard';\n onDetach?.();\n };\n\n const onKey = (str: string, key: readline.Key): void => {\n d(\n 'keypress',\n JSON.stringify({\n str,\n name: key.name,\n seq: key.sequence,\n ctrl: key.ctrl,\n meta: key.meta,\n shift: key.shift,\n mode,\n }),\n );\n const act = keymap(str, key, mode);\n d('mapped', JSON.stringify(act));\n\n if (!act) return;\n\n // eslint-disable-next-line default-case\n switch (act.type) {\n case 'CTRL_C': {\n d('CTRL_C');\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.kill('SIGINT');\n } catch {\n // noop\n }\n // optional: auto-detach so second Ctrl+C exits parent\n detach();\n return;\n }\n onCtrlC?.();\n return;\n }\n\n case 'ATTACH': {\n d('ATTACH', `id=${act.id}`, `has=${workers.has(act.id)}`);\n\n if (mode !== 'dashboard') return;\n // eslint-disable-next-line no-void\n if (workers.has(act.id)) void attach(act.id);\n return;\n }\n\n case 'CYCLE': {\n d('CYCLE', `delta=${act.delta}`);\n if (mode !== 'dashboard') return;\n const next = cycleWorkers(getWorkerIds(workers), focus, act.delta);\n // eslint-disable-next-line no-void\n if (next != null) void attach(next);\n return;\n }\n\n case 'QUIT': {\n if (mode !== 'dashboard') return;\n onCtrlC?.();\n return;\n }\n\n case 'DETACH': {\n d('DETACH');\n if (mode === 'attached') detach();\n return;\n }\n\n case 'CTRL_D': {\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.stdin?.end();\n } catch {\n // noop\n }\n }\n return;\n }\n\n case 'FORWARD': {\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.stdin?.write(act.sequence);\n } catch {\n // noop\n }\n }\n }\n }\n };\n\n // Raw bytes fallback (usually not hit because keypress handles it)\n const onData = (chunk: Buffer): void => {\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.stdin?.write(chunk);\n } catch {\n // noop\n }\n }\n };\n\n const cleanup = (): void => {\n stdin.off('keypress', onKey);\n stdin.off('data', onData);\n stdin.setRawMode?.(false);\n stdout.write('\\x1b[?25h');\n };\n\n stdin.on('keypress', onKey);\n stdin.on('data', onData);\n\n return cleanup;\n}\n","// lib/pooling/dashboardPlugin.ts\nimport * as readline from 'node:readline';\n\nimport type { ObjByString } from '@transcend-io/type-utils';\nimport type { SlotState } from '@transcend-io/utils';\nimport colors from 'colors';\n\n/**\n * A dashboard plugin defines how to render the worker pool UI.\n * Commands can supply a plugin to customize:\n * - The header block (summary stats, title, etc.)\n * - Per-worker rows (one line per worker slot)\n * - Optional extras (artifact exports, breakdowns, footers)\n *\n * @template TTotals - The shape of the aggregate totals object maintained by the command.\n */\nexport interface DashboardPlugin<TTotals, TSlotState extends ObjByString> {\n /**\n * Render the header block of the dashboard.\n *\n * @param ctx - Context with pool/worker state, totals, and metadata.\n * @returns An array of strings, each representing one line in the header.\n */\n renderHeader: (ctx: CommonCtx<TTotals, TSlotState>) => string[];\n\n /**\n * Render per-worker rows, usually one line per worker slot.\n *\n * @param ctx - Context with pool/worker state, totals, and metadata.\n * @returns An array of strings, each representing one row in the workers section.\n */\n renderWorkers: (ctx: CommonCtx<TTotals, TSlotState>) => string[];\n\n /**\n * Render any optional extra blocks that appear after the worker rows.\n * Useful for printing export paths, aggregated metrics, breakdowns, etc.\n *\n * @param ctx - Context with pool/worker state, totals, and metadata.\n * @returns An array of strings, each representing one additional line.\n */\n renderExtras?: (ctx: CommonCtx<TTotals, TSlotState>) => string[];\n}\n\n/**\n * Shared context object passed into all render methods of a {@link DashboardPlugin}.\n *\n * @template TTotals - The shape of the aggregate totals object maintained by the command.\n */\nexport type CommonCtx<TTotals, TSlotState extends ObjByString> = {\n /** Human-readable title for the dashboard (e.g., \"Parallel uploader\"). */\n title: string;\n\n /** Number of worker processes spawned in the pool. */\n poolSize: number;\n\n /** Logical CPU count, included for informational display. */\n cpuCount: number;\n\n /** Total number of \"files\" or logical units the command expects to process. */\n filesTotal: number;\n\n /** Count of successfully completed files/tasks. */\n filesCompleted: number;\n\n /** Count of failed files/tasks. */\n filesFailed: number;\n\n /**\n * State of each worker slot, keyed by worker id.\n * Includes busy flag, file label, start time, last log badge, and progress.\n */\n workerState: Map<number, SlotState<TSlotState>>;\n\n /**\n * Aggregate totals maintained by the command’s hook logic.\n * Domain-specific metrics (e.g., rows uploaded, bytes processed) can be surfaced here.\n */\n totals: TTotals;\n\n /**\n * Throughput metrics tracked by the runner:\n * - successSoFar: convenience alias for completed count\n * - r10s: completions/sec averaged over last 10s\n * - r60s: completions/sec averaged over last 60s\n */\n throughput: {\n /** Cumulative count of successful completions so far. */\n successSoFar: number;\n /** Recent file-level throughput rate over the last 10 seconds. */\n r10s: number;\n /** Recent file-level throughput rate over the last 60 seconds. */\n r60s: number;\n /** Recent job/record-level throughput rate over the last 10 seconds. */\n jobsR10s: number;\n /** Recent job/record-level throughput rate over the last 60 seconds. */\n jobsR60s: number;\n };\n\n /** True when the pool has fully drained and all workers have exited. */\n final: boolean;\n\n /**\n * Optional export status payload provided by the command.\n * Useful for rendering artifact paths or \"latest export\" summaries.\n */\n exportStatus?: Record<string, unknown>;\n};\n\n/** The most recently rendered frame, cached to suppress flicker from duplicate renders. */\nlet lastFrame = '';\n\n/**\n * Generate the hotkeys hint string that appears at the bottom of the dashboard.\n *\n * @param poolSize - The number of worker slots in the pool.\n * @param final - Whether the run has completed.\n * @returns A dimmed string listing the supported hotkeys for attach/detach/quit.\n */\nexport const hotkeysHint = (poolSize: number, final: boolean): string => {\n const maxDigit = Math.min(poolSize - 1, 9);\n const digitRange = poolSize <= 1 ? '0' : `0-${maxDigit}`;\n const extra = poolSize > 10 ? ' (Tab/Shift+Tab for ≥10)' : '';\n return final\n ? colors.dim(\n 'Run complete — digits to view logs • Tab/Shift+Tab cycle • Esc/Ctrl+] detach • q to quit',\n )\n : colors.dim(\n `Hotkeys: [${digitRange}] attach${extra} • e=errors • w=warnings • i=info • l=logs • ` +\n 'Tab/Shift+Tab • Esc/Ctrl+] detach • Ctrl+C exit',\n );\n};\n\n/**\n * Render the dashboard using a supplied {@link DashboardPlugin}.\n *\n * The frame is composed of:\n * - Header lines\n * - A blank separator\n * - Worker rows\n * - A blank separator\n * - Hotkeys hint\n * - Optional extras (if plugin supplies them)\n *\n * Optimizations:\n * - Suppresses re-renders if the frame is identical to the previous frame (flicker-free).\n * - Hides the terminal cursor during live updates, restoring it when final.\n *\n * @param ctx - Shared context containing pool state, worker state, totals, throughput, etc.\n * @param plugin - The plugin that defines how to render the header, workers, and optional extras.\n * @param viewerMode - If true, renders in viewer mode (no ability to switch between files).\n */\nexport function dashboardPlugin<TTotals, TSlotState extends ObjByString>(\n ctx: CommonCtx<TTotals, TSlotState>,\n plugin: DashboardPlugin<TTotals, TSlotState>,\n viewerMode = false,\n): void {\n const frame = [\n ...plugin.renderHeader(ctx),\n '',\n ...plugin.renderWorkers(ctx),\n ...(viewerMode ? [] : ['', hotkeysHint(ctx.poolSize, ctx.final)]),\n ...(plugin.renderExtras ? [''].concat(plugin.renderExtras(ctx)) : []),\n ].join('\\n');\n\n // Skip duplicate renders during live runs to avoid flicker.\n if (!ctx.final && frame === lastFrame) return;\n lastFrame = frame;\n\n if (!ctx.final) {\n // Hide cursor and repaint in place\n process.stdout.write('\\x1b[?25l');\n readline.cursorTo(process.stdout, 0, 0);\n readline.clearScreenDown(process.stdout);\n } else {\n // Restore cursor on final render\n process.stdout.write('\\x1b[?25h');\n }\n process.stdout.write(`${frame}\\n`);\n}\n","import { basename } from 'node:path';\n\nimport type { ObjByString } from '@transcend-io/type-utils';\nimport colors from 'colors';\n\nimport type { CommonCtx } from './dashboardPlugin.js';\n\n/**\n * Progress snapshot for a worker slot in the chunk-csv command.\n */\nexport type ChunkSlotProgress = {\n /** Absolute path of the file being processed by this worker. */\n filePath?: string;\n /** Number of rows processed so far in this file. */\n processed?: number;\n /** Optional total number of rows in the file (if known). */\n total?: number;\n};\n\n/**\n * Format a number safely for display.\n *\n * @param n - The number to format (or `undefined`).\n * @returns A localized string representation, or \"0\".\n */\nexport function fmtNum(n: number | undefined): string {\n return typeof n === 'number' ? n.toLocaleString() : '0';\n}\n\n/**\n * Draw a horizontal bar of length `width` filled to `pct` percent.\n *\n * @param pct - Percentage 0..100.\n * @param width - Number of characters in the bar.\n * @returns A string like \"████░░░░\".\n */\nexport function pctBar(pct: number, width = 40): string {\n const clamped = Math.max(0, Math.min(100, Math.floor(pct)));\n const filled = Math.floor((clamped / 100) * width);\n return '█'.repeat(filled) + '░'.repeat(width - filled);\n}\n\n/**\n * Compute pool-wide progress values needed by headers.\n *\n * @param ctx - Dashboard context containing pool state, worker state, totals, etc.\n * @returns An object with `done`, `inProgress`, and `pct` properties.\n */\nexport function poolProgress<TTotals, TSlot extends ObjByString>(\n ctx: CommonCtx<TTotals, TSlot>,\n): {\n /** Count of successfully completed files/tasks. */\n done: number;\n /** Count of currently in-progress files/tasks. */\n inProgress: number;\n /** Percentage of completion (0-100). */\n pct: number;\n} {\n const inProgress = [...ctx.workerState.values()].filter((s) => s.busy).length;\n const done = ctx.filesCompleted + ctx.filesFailed;\n const pct = ctx.filesTotal === 0 ? 100 : Math.floor((done / Math.max(1, ctx.filesTotal)) * 100);\n return { done, inProgress, pct };\n}\n\n/**\n * Compose the common header lines (title, pool stats, progress bar, throughput).\n *\n * @param ctx - Dashboard context.\n * @param extraLines - Optional extra lines (e.g., totals block).\n * @returns Header lines.\n */\nexport function makeHeader<TTotals, TSlot extends ObjByString>(\n ctx: CommonCtx<TTotals, TSlot>,\n extraLines: string[] = [],\n): string[] {\n const { title, poolSize, cpuCount, filesTotal, filesCompleted, filesFailed, throughput } = ctx;\n const { inProgress, pct } = poolProgress(ctx);\n\n const lines: string[] = [\n `${colors.bold(title)} — ${poolSize} workers ${colors.dim(`(CPU avail: ${cpuCount})`)}`,\n `${colors.dim('Files')} ${fmtNum(filesTotal)} ${colors.dim(\n 'Completed',\n )} ${fmtNum(filesCompleted)} ${colors.dim('Failed')} ${\n filesFailed ? colors.red(fmtNum(filesFailed)) : fmtNum(filesFailed)\n } ${colors.dim('In-flight')} ${fmtNum(inProgress)}`,\n `[${pctBar(pct)}] ${pct}%`,\n ];\n\n if (throughput) {\n const jobsActive = throughput.jobsR10s > 0 || throughput.jobsR60s > 0;\n const perHour10 = Math.round(\n (jobsActive ? throughput.jobsR10s : throughput.r10s) * 3600,\n ).toLocaleString();\n const perHour60 = Math.round(\n (jobsActive ? throughput.jobsR60s : throughput.r60s) * 3600,\n ).toLocaleString();\n const unit = jobsActive ? 'rec' : 'files';\n const suffix =\n ctx.throughput?.successSoFar != null\n ? ` Newly uploaded: ${fmtNum(ctx.throughput.successSoFar)}`\n : '';\n lines.push(\n colors.cyan(`Throughput: ${perHour10} ${unit}/hr (1h: ${perHour60} ${unit}/hr)${suffix}`),\n );\n }\n\n return extraLines.length ? lines.concat(extraLines) : lines;\n}\n\n/**\n * Render per-worker rows with a compact progress bar and status badge.\n *\n * @param ctx - Dashboard context (slot progress type must have processed/total?).\n * @param getFileLabel - Optional: override how the filename is shown.\n * @returns Array of strings, each representing one worker row.\n */\nexport function makeWorkerRows<TTotals, TSlot extends Omit<ChunkSlotProgress, 'filePath'>>(\n ctx: CommonCtx<TTotals, TSlot>,\n getFileLabel: (file: string | null | undefined) => string = (file) =>\n file ? basename(file) : '-',\n): string[] {\n const miniWidth = 18;\n\n return [...ctx.workerState.entries()].map(([id, s]) => {\n const badge =\n s.lastLevel === 'error'\n ? colors.red('ERROR ')\n : s.lastLevel === 'warn'\n ? colors.yellow('WARN ')\n : s.busy\n ? colors.green('WORKING')\n : colors.dim('IDLE ');\n\n const fname = getFileLabel(s.file);\n const elapsed = s.startedAt ? `${Math.floor((Date.now() - s.startedAt) / 1000)}s` : '-';\n\n const processed = s.progress?.processed ?? 0;\n const total = s.progress?.total ?? 0;\n const pctw = total > 0 ? Math.floor((processed / total) * 100) : 0;\n const mini = total > 0 ? pctBar(pctw, miniWidth) : ' '.repeat(miniWidth);\n const miniTxt =\n total > 0\n ? `${processed.toLocaleString()}/${total.toLocaleString()} (${pctw}%)`\n : colors.dim('—');\n\n return ` [w${id}] ${badge} | ${fname} | ${elapsed} | [${mini}] ${miniTxt}`;\n });\n}\n","import type { ExportStatusMap, SlotPaths } from '@transcend-io/utils';\n\nimport { showCombinedLogs, type LogLocation } from './showCombinedLogs.js';\n\n/** Severity filter applied by the viewer. */\ntype ViewLevel = 'error' | 'warn' | 'all';\n\n/**\n * Options for {@link createExtraKeyHandler}.\n */\nexport type CreateExtraKeyHandlerOpts = {\n /**\n * Per-slot log file paths maintained by the runner; used to stream or export logs.\n */\n logsBySlot: SlotPaths;\n\n /**\n * Request an immediate dashboard repaint (e.g., after updating export status).\n */\n repaint: () => void;\n\n /**\n * Pause/unpause dashboard repainting. The handler pauses while a viewer is open\n * to prevent the dashboard from overwriting the viewer output, then resumes on exit.\n */\n setPaused: (p: boolean) => void;\n\n /**\n * Optional export manager to enable uppercase export keys:\n * - `E` (errors) • `W` (warnings) • `I` (info) • `A` (all)\n *\n * Provide this only if your command supports writing combined log files.\n */\n exportMgr?: {\n /** Destination directory for exported artifacts. */\n exportsDir: string;\n /**\n * Write a combined log file for the selected severity and return the absolute path.\n *\n * @param logs - Log paths to combine.\n * @param which - Severity selection.\n * @returns Absolute path to the written file.\n */\n exportCombinedLogs: (logs: SlotPaths, which: 'error' | 'warn' | 'info' | 'all') => string;\n };\n\n /**\n * Optional “Exports” status map. If provided, the handler updates timestamps\n * when exports are written so your dashboard panel can reflect “last saved” times.\n */\n exportStatus?: ExportStatusMap;\n\n /**\n * Optional custom key bindings for command-specific actions.\n * Each handler receives helpers to print messages and to update the exports panel.\n *\n * Example:\n * ```ts\n * custom: {\n * F: async ({ say, noteExport }) => {\n * const p = await writeFailingUpdatesCsv(...);\n * say(`Wrote failing updates to: ${p}`);\n * noteExport('failuresCsv', p);\n * }\n * }\n * ```\n */\n custom?: Record<\n string,\n (ctx: {\n /** Update {@link exportStatus} (if present) and repaint the dashboard. */\n noteExport: (slot: keyof ExportStatusMap, absPath: string) => void;\n /** Print a line to stdout, automatically newline-terminated. */\n say: (s: string) => void;\n }) => void | Promise<void>\n >;\n};\n\n/**\n * Create a keypress handler for interactive viewers/exports.\n * Shared handler for \"extra\" keyboard shortcuts used by the interactive dashboard.\n *\n * It wires:\n * - **Viewers (lowercase):** `e` (errors), `w` (warnings), `i` (info), `l` (all)\n * - **Exports (uppercase, optional):** `E` (errors), `W` (warnings), `I` (info), `A` (all)\n * - **Dismiss:** `Esc` or `Ctrl+]` exits a viewer and returns to the dashboard\n * - **Custom keys (optional):** Provide a `custom` map to handle command-specific bindings\n *\n * Usage (inside `runPool({... extraKeyHandler })`):\n * ```ts\n * extraKeyHandler: ({ logsBySlot, repaint, setPaused }) =>\n * createExtraKeyHandler({ logsBySlot, repaint, setPaused })\n * ```\n *\n * If you also want export hotkeys + an \"Exports\" panel:\n * ```ts\n * extraKeyHandler: ({ logsBySlot, repaint, setPaused }) =>\n * createExtraKeyHandler({\n * logsBySlot, repaint, setPaused,\n * exportMgr, // enables E/W/I/A\n * exportStatus, // keeps panel timestamps up to date\n * custom: { // optional, e.g. 'F' to export a CSV\n * F: async ({ say, noteExport }) => { ... }\n * }\n * })\n * ```\n *\n * @param opts - Configuration for viewers, exports, and custom keys.\n * @returns A `(buf: Buffer) => void` handler suitable for `process.stdin.on('data', ...)`.\n */\nexport function createExtraKeyHandler(opts: CreateExtraKeyHandlerOpts): (buf: Buffer) => void {\n const { logsBySlot, repaint, setPaused, exportMgr, exportStatus, custom } = opts;\n\n const say = (s: string): void => {\n process.stdout.write(`${s}\\n`);\n };\n\n /**\n * Record that an export was written and trigger a repaint so the dashboard’s\n * \"Exports\" panel shows the updated timestamp/path.\n *\n * @param slot - Slot name in {@link ExportStatusMap} (e.g., \"error\", \"warn\", etc.).\n * @param p - Absolute path to the exported file.\n */\n const noteExport = (slot: keyof ExportStatusMap, p: string): void => {\n const now = Date.now();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const cur: any = exportStatus?.[slot] ?? { path: p };\n if (exportStatus) {\n exportStatus[slot] = {\n path: p || cur.path,\n savedAt: now,\n exported: true,\n };\n repaint();\n }\n };\n\n let viewing = false; // optional guard to prevent stacking viewers\n\n /**\n * Show an inline combined log viewer for the selected sources/level.\n * Pauses dashboard repaint to keep the viewer visible until the user exits.\n *\n * @param sources - Log sources to include (e.g., \"err\", \"warn\", \"info\").\n * @param level - Severity level to filter by (e.g., \"error\", \"warn\", \"all\").\n */\n const view = (sources: LogLocation[], level: ViewLevel): void => {\n if (viewing) return;\n viewing = true;\n setPaused(true);\n\n // optional UX: clear screen and show a hint\n process.stdout.write('\\x1b[2J\\x1b[H'); // clear+home\n process.stdout.write('Combined logs viewer (press Esc or Ctrl+] to return)\\n\\n');\n\n (async () => {\n try {\n await showCombinedLogs(logsBySlot, sources, level);\n // NOTE: do NOT unpause here; ESC will handle it.\n } catch {\n // If showCombinedLogs throws, recover and unpause\n viewing = false;\n setPaused(false);\n repaint();\n }\n })();\n };\n\n /**\n * Export combined logs (if an export manager was provided).\n *\n * @param which - Severity to export (e.g., \"error\", \"warn\", \"info\", \"all\").\n * @param label - Human-readable label for the export (e.g., \"error\", \"warn\").\n */\n const exportCombined = (which: 'error' | 'warn' | 'info' | 'all', label: string): void => {\n if (!exportMgr) return;\n try {\n const p = exportMgr.exportCombinedLogs(logsBySlot, which);\n say(`\\nWrote combined ${label} logs to: ${p}`);\n noteExport(which as keyof ExportStatusMap, p);\n } catch {\n say(`\\nFailed to write combined ${label} logs`);\n }\n };\n\n // The keypress handler the runner will attach to stdin.\n return (buf: Buffer): void => {\n const s = buf.toString('utf8');\n\n // Viewers (lowercase)\n if (s === 'e') {\n view(['err'], 'error');\n return;\n }\n if (s === 'w') {\n view(['warn', 'err'], 'warn');\n return;\n }\n if (s === 'i') {\n view(['info'], 'all');\n return;\n }\n if (s === 'l') {\n view(['out', 'err', 'structured'], 'all');\n return;\n }\n\n // Exports (uppercase) — enabled only when exportMgr is present\n if (s === 'E') {\n exportCombined('error', 'error');\n return;\n }\n if (s === 'W') {\n exportCombined('warn', 'warn');\n return;\n }\n if (s === 'I') {\n exportCombined('info', 'info');\n return;\n }\n if (s === 'A') {\n exportCombined('all', 'ALL');\n return;\n }\n\n // Command-specific bindings\n const fn = custom?.[s];\n if (fn) {\n fn({ noteExport, say });\n return;\n }\n\n // Exit a viewer (Esc / Ctrl+]) — resume dashboard\n if (s === '\\x1b' || s === '\\x1d') {\n viewing = false;\n setPaused(false);\n repaint();\n }\n };\n}\n"],"mappings":"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"}
|
package/dist/{downloadPrivacyRequestFiles-8DtRUNXp.mjs → downloadPrivacyRequestFiles-CqHT6HSU.mjs}
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as e}from"./constants-
|
|
2
|
-
//# sourceMappingURL=downloadPrivacyRequestFiles-
|
|
1
|
+
import{a as e}from"./constants-D22_ckyl.mjs";import{t}from"./logger-Bj782ZYD.mjs";import{t as n}from"./request-DfkRPQFr.mjs";import{r}from"./fetchAllRequests-CHHdyb4Q.mjs";import{RequestAction as i,RequestStatus as a,TableEncryptionType as o}from"@transcend-io/privacy-types";import{decodeCodec as s,valuesOf as c}from"@transcend-io/type-utils";import{existsSync as l,mkdirSync as u,writeFileSync as d}from"node:fs";import{dirname as f,join as p}from"node:path";import m from"colors";import*as h from"io-ts";import{buildTranscendGraphQLClient as g,createSombraGotInstance as _,makeGraphQLRequest as v}from"@transcend-io/sdk";import{map as y}from"@transcend-io/utils";import b from"cli-progress";const x=h.type({defaultMessage:h.string,id:h.string}),S=h.type({downloadKey:h.string,error:h.union([h.null,h.string]),mimetype:h.string,size:h.string,fileName:h.string,dataPoint:h.type({id:h.string,title:h.union([x,h.null]),description:h.union([x,h.null]),name:h.string,slug:h.string,encryption:h.union([c(o),h.null]),dataSilo:h.type({id:h.string,title:h.string,description:h.string,type:h.string,outerType:h.union([h.string,h.null])}),path:h.array(h.string)})}),C=h.type({nodes:h.array(S),totalCount:h.number,_links:h.partial({next:h.union([h.string,h.null]),previous:h.union([h.string,h.null])})});async function w(e,{sombra:n,concurrency:r=5,limit:i=100}){t.info(m.magenta(`Pulling file metadata for ${e.length} requests`));let a=new Date().getTime(),o=new b.SingleBar({},b.Presets.shades_classic),c=0;o.start(e.length,0);let l=await y(e,async e=>{let t=[],r=!0,a=0;for(;r;){let o;try{o=s(C,await n.get(`v1/data-subject-request/${e.id}/download-keys`,{searchParams:{limit:i,offset:a}}).json()),t.push(...o.nodes),a+=i,r=!!o._links.next&&o.nodes.length===i}catch(e){throw Error(`Received an error from server: ${e?.response?.body||e?.message}`)}}return c+=1,o.update(c),[e,t]},{concurrency:r});o.stop();let u=new Date().getTime()-a;return t.info(m.green(`Successfully downloaded file metadata ${e.length} requests in "${u/1e3}" seconds!`)),l}async function T(e,{requestId:n,sombra:r,onFileDownloaded:i,concurrency:a=20}){await y(e,async e=>{try{await r.get(`v1/files`,{searchParams:{downloadKey:e.downloadKey}}).buffer().then(t=>i(e,t))}catch(r){if(r?.response?.body?.includes(`fileMetadata#verify`)){t.error(m.red(`Failed to pull file for: ${e.fileName} (request:${n}) - JWT expired. This likely means that the file is no longer available. Try restarting the request from scratch in Transcend Admin Dashboard. Skipping the download of this file.`));return}throw Error(`Received an error from server: ${r?.response?.body||r?.message}`)}},{concurrency:a})}async function E({auth:o,folderPath:s,requestIds:c,createdAtBefore:h,sombraAuth:x,createdAtAfter:S,updatedAtBefore:C,updatedAtAfter:E,statuses:D=[a.Approving,a.Downloadable],concurrency:O=5,transcendUrl:k=e,approveAfterDownload:A=!1}){let j=g(k,o),M=await _(k,o,{logger:t,sombraApiKey:x,sombraUrl:process.env.SOMBRA_URL});l(s)||u(s);let N=await r(j,{actions:[i.Access],createdAtBefore:h,createdAtAfter:S,updatedAtBefore:C,updatedAtAfter:E,statuses:D,requestIds:c}),P=await w(N,{sombra:M,concurrency:O}),F=new Date().getTime(),I=new b.SingleBar({},b.Presets.shades_classic),L=0,R=0;I.start(N.length,0),await y(P,async([e,r])=>{let i=p(s,e.id);l(i)||u(i),await T(r,{sombra:M,requestId:e.id,onFileDownloaded:(e,t)=>{let n=p(i,e.fileName),r=f(n);l(r)||u(r,{recursive:!0}),d(n,t)}}),A&&e.status===a.Approving&&(await v(j,n,{variables:{input:{requestId:e.id}},logger:t}),R+=1),L+=1,I.update(L)},{concurrency:O}),I.stop();let z=new Date().getTime()-F;return t.info(m.green(`Successfully downloaded ${L} requests in "${z/1e3}" seconds!`)),R>0&&t.info(m.green(`Approved ${R} requests in Transcend.`)),N.length}export{C as a,S as i,T as n,w as o,x as r,E as t};
|
|
2
|
+
//# sourceMappingURL=downloadPrivacyRequestFiles-CqHT6HSU.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"downloadPrivacyRequestFiles-8DtRUNXp.mjs","names":[],"sources":["../src/lib/requests/getFileMetadataForPrivacyRequests.ts","../src/lib/requests/streamPrivacyRequestFiles.ts","../src/lib/requests/downloadPrivacyRequestFiles.ts"],"sourcesContent":["import { TableEncryptionType } from '@transcend-io/privacy-types';\nimport { decodeCodec, valuesOf } from '@transcend-io/type-utils';\nimport { map } from '@transcend-io/utils';\nimport cliProgress from 'cli-progress';\nimport colors from 'colors';\nimport type { Got } from 'got';\nimport * as t from 'io-ts';\n\nimport { logger } from '../../logger.js';\nimport { PrivacyRequest } from '../graphql/index.js';\n\nexport const IntlMessage = t.type({\n /** The message key */\n defaultMessage: t.string,\n /** ID */\n id: t.string,\n});\n\n/** Type */\nexport type IntlMessage = t.TypeOf<typeof IntlMessage>;\n\nexport const RequestFileMetadata = t.type({\n /** The key to pass to download the file contents */\n downloadKey: t.string,\n /** Error message related to file */\n error: t.union([t.null, t.string]),\n /** Mimetype of file */\n mimetype: t.string,\n /** Size of file, stored as string as this can be a BigInt */\n size: t.string,\n /** Name of file based on datapoint names in Transcend */\n fileName: t.string,\n /** The metadata on the datapoint */\n dataPoint: t.type({\n /** ID of datapoint */\n id: t.string,\n /** The title of datapoint */\n title: t.union([IntlMessage, t.null]),\n /** Description of datapoint */\n description: t.union([IntlMessage, t.null]),\n /** Name of datapoint */\n name: t.string,\n /** Slug of datapoint */\n slug: t.string,\n /** Table level encryption information */\n encryption: t.union([valuesOf(TableEncryptionType), t.null]),\n /** The name of the data silo */\n dataSilo: t.type({\n /** ID of the data silo */\n id: t.string,\n /** The title of the data silo */\n title: t.string,\n /** The description of the data silo */\n description: t.string,\n /** The type of the data silo */\n type: t.string,\n /** The outer type of the data silo */\n outerType: t.union([t.string, t.null]),\n }),\n /** The path to the datapoint if a database (e.g. name of schema) */\n path: t.array(t.string),\n }),\n});\n\n/** Type override */\nexport type RequestFileMetadata = t.TypeOf<typeof RequestFileMetadata>;\n\nexport const RequestFileMetadataResponse = t.type({\n /** The list of file metadata */\n nodes: t.array(RequestFileMetadata),\n /** The total number of file metadata */\n totalCount: t.number,\n /** Links to next pages */\n _links: t.partial({\n /** The link to the next page of file metadata */\n next: t.union([t.string, t.null]),\n /** The link to the previous page of file metadata */\n previous: t.union([t.string, t.null]),\n }),\n});\n\n/** Type override */\nexport type RequestFileMetadataResponse = t.TypeOf<typeof RequestFileMetadataResponse>;\n\n/**\n * Given a list of privacy requests, download the file metadata\n * for these requests - this is useful to prepare the files in a\n * data access request for download.\n *\n * @param requests - The list of privacy requests to download files for\n * @param options - Options\n * @returns The number of requests canceled\n */\nexport async function getFileMetadataForPrivacyRequests(\n requests: Pick<PrivacyRequest, 'id' | 'status'>[],\n {\n sombra,\n concurrency = 5,\n limit = 100,\n }: {\n /** Sombra instance */\n sombra: Got;\n /** Number of files to pull at once */\n limit?: number;\n /** Concurrency limit for approving */\n concurrency?: number;\n },\n): Promise<[Pick<PrivacyRequest, 'id' | 'status'>, RequestFileMetadata[]][]> {\n logger.info(colors.magenta(`Pulling file metadata for ${requests.length} requests`));\n\n // Time duration\n const t0 = new Date().getTime();\n // create a new progress bar instance and use shades_classic theme\n const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);\n\n // Start timer\n let total = 0;\n progressBar.start(requests.length, 0);\n\n // Loop over the requests\n const results = await map(\n requests,\n async (\n requestToDownload,\n ): Promise<[Pick<PrivacyRequest, 'id' | 'status'>, RequestFileMetadata[]]> => {\n const localResults: RequestFileMetadata[] = [];\n\n // Paginate over the file metadata for this request\n let shouldContinue = true;\n let offset = 0;\n while (shouldContinue) {\n let response: RequestFileMetadataResponse;\n try {\n // Grab the file metadata for this request\n\n const rawResponse = await sombra\n .get(`v1/data-subject-request/${requestToDownload.id}/download-keys`, {\n searchParams: {\n limit,\n offset,\n },\n })\n .json();\n response = decodeCodec(RequestFileMetadataResponse, rawResponse);\n localResults.push(...response.nodes);\n\n // Increase offset and break if no more pages\n offset += limit;\n shouldContinue =\n // eslint-disable-next-line no-underscore-dangle\n !!response._links.next && response.nodes.length === limit;\n } catch (err) {\n throw new Error(`Received an error from server: ${err?.response?.body || err?.message}`);\n }\n }\n\n total += 1;\n progressBar.update(total);\n return [requestToDownload, localResults];\n },\n { concurrency },\n );\n\n progressBar.stop();\n const t1 = new Date().getTime();\n const totalTime = t1 - t0;\n\n logger.info(\n colors.green(\n `Successfully downloaded file metadata ${requests.length} requests in \"${\n totalTime / 1000\n }\" seconds!`,\n ),\n );\n\n return results;\n}\n","import { map } from '@transcend-io/utils';\nimport colors from 'colors';\nimport type { Got } from 'got';\n\nimport { logger } from '../../logger.js';\nimport { RequestFileMetadata } from './getFileMetadataForPrivacyRequests.js';\n\n/**\n * This function will take in a set of file metadata for privacy requests\n * call the Transcend API to stream the file metadata for these requests\n * and pass that through a callback function\n *\n * @param fileMetadata - Metadata to download\n * @param options - Options for the request\n */\nexport async function streamPrivacyRequestFiles(\n fileMetadata: RequestFileMetadata[],\n {\n requestId,\n sombra,\n onFileDownloaded,\n concurrency = 20,\n }: {\n /** Request ID for logging */\n requestId: string;\n /** Sombra got instance */\n sombra: Got;\n /** Handler on each file */\n onFileDownloaded: (metadata: RequestFileMetadata, stream: Buffer) => void;\n /** Concurrent downloads at once */\n concurrency?: number;\n },\n): Promise<void> {\n // Loop over each file\n await map(\n fileMetadata,\n async (metadata) => {\n try {\n // Construct the stream\n await sombra\n .get('v1/files', {\n searchParams: {\n downloadKey: metadata.downloadKey,\n },\n })\n .buffer()\n .then((fileResponse) => onFileDownloaded(metadata, fileResponse));\n } catch (err) {\n if (err?.response?.body?.includes('fileMetadata#verify')) {\n logger.error(\n colors.red(\n `Failed to pull file for: ${metadata.fileName} (request:${requestId}) - JWT expired. ` +\n 'This likely means that the file is no longer available. ' +\n 'Try restarting the request from scratch in Transcend Admin Dashboard. ' +\n 'Skipping the download of this file.',\n ),\n );\n return;\n }\n throw new Error(`Received an error from server: ${err?.response?.body || err?.message}`);\n }\n },\n {\n concurrency,\n },\n );\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport { RequestAction, RequestStatus } from '@transcend-io/privacy-types';\nimport {\n buildTranscendGraphQLClient,\n createSombraGotInstance,\n makeGraphQLRequest,\n} from '@transcend-io/sdk';\nimport { map } from '@transcend-io/utils';\nimport cliProgress from 'cli-progress';\nimport colors from 'colors';\n\nimport { DEFAULT_TRANSCEND_API } from '../../constants.js';\nimport { logger } from '../../logger.js';\nimport { fetchAllRequests, APPROVE_PRIVACY_REQUEST } from '../graphql/index.js';\nimport { getFileMetadataForPrivacyRequests } from './getFileMetadataForPrivacyRequests.js';\nimport { streamPrivacyRequestFiles } from './streamPrivacyRequestFiles.js';\n\n/**\n * Download a set of privacy requests to disk\n *\n * @param options - Options\n * @returns The number of requests canceled\n */\nexport async function downloadPrivacyRequestFiles({\n auth,\n folderPath,\n requestIds,\n createdAtBefore,\n sombraAuth,\n createdAtAfter,\n updatedAtBefore,\n updatedAtAfter,\n statuses = [RequestStatus.Approving, RequestStatus.Downloadable],\n concurrency = 5,\n transcendUrl = DEFAULT_TRANSCEND_API,\n approveAfterDownload = false,\n}: {\n /** The folder path to download the files to */\n folderPath: string;\n /** Transcend API key authentication */\n auth: string;\n /** Sombra API key authentication */\n sombraAuth?: string;\n /** Concurrency limit for approving */\n concurrency?: number;\n /** The request statuses to cancel */\n statuses?: RequestStatus[];\n /** The set of privacy requests to cancel */\n requestIds?: string[];\n /** Filter for requests created before this date */\n createdAtBefore?: Date;\n /** Filter for requests created after this date */\n createdAtAfter?: Date;\n /** Filter for requests updated before this date */\n updatedAtBefore?: Date;\n /** Filter for requests updated after this date */\n updatedAtAfter?: Date;\n /** API URL for Transcend backend */\n transcendUrl?: string;\n /** When true, approve any requests in Transcend that are in status=APPROVING */\n approveAfterDownload?: boolean;\n}): Promise<number> {\n // Find all requests made before createdAt that are in a removing data state\n const client = buildTranscendGraphQLClient(transcendUrl, auth);\n\n // Create sombra instance to communicate with\n const sombra = await createSombraGotInstance(transcendUrl, auth, {\n logger,\n sombraApiKey: sombraAuth,\n sombraUrl: process.env.SOMBRA_URL,\n });\n\n // Create the folder if it does not exist\n if (!existsSync(folderPath)) {\n mkdirSync(folderPath);\n }\n\n // Pull in the requests\n const allRequests = await fetchAllRequests(client, {\n actions: [RequestAction.Access],\n createdAtBefore,\n createdAtAfter,\n updatedAtBefore,\n updatedAtAfter,\n statuses,\n requestIds,\n });\n\n // Download the file metadata for each request\n const requestFileMetadata = await getFileMetadataForPrivacyRequests(allRequests, {\n sombra,\n concurrency,\n });\n\n // Start timer for download process\n const t0 = new Date().getTime();\n const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);\n let total = 0;\n let totalApproved = 0;\n progressBar.start(allRequests.length, 0);\n\n // Download the files for each request\n await map(\n requestFileMetadata,\n async ([request, metadata]) => {\n // Create a new folder to store request files\n const requestFolder = join(folderPath, request.id);\n if (!existsSync(requestFolder)) {\n mkdirSync(requestFolder);\n }\n\n // Stream each file to disk\n await streamPrivacyRequestFiles(metadata, {\n sombra,\n requestId: request.id,\n onFileDownloaded: (fil, stream) => {\n // Ensure a folder exists for the file\n // filename looks like Health/heartbeat.csv\n const filePath = join(requestFolder, fil.fileName);\n const folder = dirname(filePath);\n if (!existsSync(folder)) {\n mkdirSync(folder, { recursive: true });\n }\n\n // Write to disk\n writeFileSync(filePath, stream);\n },\n });\n\n // Approve the request if requested\n if (approveAfterDownload && request.status === RequestStatus.Approving) {\n await makeGraphQLRequest(client, APPROVE_PRIVACY_REQUEST, {\n variables: { input: { requestId: request.id } },\n logger,\n });\n totalApproved += 1;\n }\n\n // Increment the progress bar\n total += 1;\n progressBar.update(total);\n },\n { concurrency },\n );\n\n progressBar.stop();\n const t1 = new Date().getTime();\n const totalTime = t1 - t0;\n\n logger.info(\n colors.green(`Successfully downloaded ${total} requests in \"${totalTime / 1000}\" seconds!`),\n );\n if (totalApproved > 0) {\n logger.info(colors.green(`Approved ${totalApproved} requests in Transcend.`));\n }\n return allRequests.length;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"downloadPrivacyRequestFiles-CqHT6HSU.mjs","names":[],"sources":["../src/lib/requests/getFileMetadataForPrivacyRequests.ts","../src/lib/requests/streamPrivacyRequestFiles.ts","../src/lib/requests/downloadPrivacyRequestFiles.ts"],"sourcesContent":["import { TableEncryptionType } from '@transcend-io/privacy-types';\nimport { decodeCodec, valuesOf } from '@transcend-io/type-utils';\nimport { map } from '@transcend-io/utils';\nimport cliProgress from 'cli-progress';\nimport colors from 'colors';\nimport type { Got } from 'got';\nimport * as t from 'io-ts';\n\nimport { logger } from '../../logger.js';\nimport { PrivacyRequest } from '../graphql/index.js';\n\nexport const IntlMessage = t.type({\n /** The message key */\n defaultMessage: t.string,\n /** ID */\n id: t.string,\n});\n\n/** Type */\nexport type IntlMessage = t.TypeOf<typeof IntlMessage>;\n\nexport const RequestFileMetadata = t.type({\n /** The key to pass to download the file contents */\n downloadKey: t.string,\n /** Error message related to file */\n error: t.union([t.null, t.string]),\n /** Mimetype of file */\n mimetype: t.string,\n /** Size of file, stored as string as this can be a BigInt */\n size: t.string,\n /** Name of file based on datapoint names in Transcend */\n fileName: t.string,\n /** The metadata on the datapoint */\n dataPoint: t.type({\n /** ID of datapoint */\n id: t.string,\n /** The title of datapoint */\n title: t.union([IntlMessage, t.null]),\n /** Description of datapoint */\n description: t.union([IntlMessage, t.null]),\n /** Name of datapoint */\n name: t.string,\n /** Slug of datapoint */\n slug: t.string,\n /** Table level encryption information */\n encryption: t.union([valuesOf(TableEncryptionType), t.null]),\n /** The name of the data silo */\n dataSilo: t.type({\n /** ID of the data silo */\n id: t.string,\n /** The title of the data silo */\n title: t.string,\n /** The description of the data silo */\n description: t.string,\n /** The type of the data silo */\n type: t.string,\n /** The outer type of the data silo */\n outerType: t.union([t.string, t.null]),\n }),\n /** The path to the datapoint if a database (e.g. name of schema) */\n path: t.array(t.string),\n }),\n});\n\n/** Type override */\nexport type RequestFileMetadata = t.TypeOf<typeof RequestFileMetadata>;\n\nexport const RequestFileMetadataResponse = t.type({\n /** The list of file metadata */\n nodes: t.array(RequestFileMetadata),\n /** The total number of file metadata */\n totalCount: t.number,\n /** Links to next pages */\n _links: t.partial({\n /** The link to the next page of file metadata */\n next: t.union([t.string, t.null]),\n /** The link to the previous page of file metadata */\n previous: t.union([t.string, t.null]),\n }),\n});\n\n/** Type override */\nexport type RequestFileMetadataResponse = t.TypeOf<typeof RequestFileMetadataResponse>;\n\n/**\n * Given a list of privacy requests, download the file metadata\n * for these requests - this is useful to prepare the files in a\n * data access request for download.\n *\n * @param requests - The list of privacy requests to download files for\n * @param options - Options\n * @returns The number of requests canceled\n */\nexport async function getFileMetadataForPrivacyRequests(\n requests: Pick<PrivacyRequest, 'id' | 'status'>[],\n {\n sombra,\n concurrency = 5,\n limit = 100,\n }: {\n /** Sombra instance */\n sombra: Got;\n /** Number of files to pull at once */\n limit?: number;\n /** Concurrency limit for approving */\n concurrency?: number;\n },\n): Promise<[Pick<PrivacyRequest, 'id' | 'status'>, RequestFileMetadata[]][]> {\n logger.info(colors.magenta(`Pulling file metadata for ${requests.length} requests`));\n\n // Time duration\n const t0 = new Date().getTime();\n // create a new progress bar instance and use shades_classic theme\n const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);\n\n // Start timer\n let total = 0;\n progressBar.start(requests.length, 0);\n\n // Loop over the requests\n const results = await map(\n requests,\n async (\n requestToDownload,\n ): Promise<[Pick<PrivacyRequest, 'id' | 'status'>, RequestFileMetadata[]]> => {\n const localResults: RequestFileMetadata[] = [];\n\n // Paginate over the file metadata for this request\n let shouldContinue = true;\n let offset = 0;\n while (shouldContinue) {\n let response: RequestFileMetadataResponse;\n try {\n // Grab the file metadata for this request\n\n const rawResponse = await sombra\n .get(`v1/data-subject-request/${requestToDownload.id}/download-keys`, {\n searchParams: {\n limit,\n offset,\n },\n })\n .json();\n response = decodeCodec(RequestFileMetadataResponse, rawResponse);\n localResults.push(...response.nodes);\n\n // Increase offset and break if no more pages\n offset += limit;\n shouldContinue =\n // eslint-disable-next-line no-underscore-dangle\n !!response._links.next && response.nodes.length === limit;\n } catch (err) {\n throw new Error(`Received an error from server: ${err?.response?.body || err?.message}`);\n }\n }\n\n total += 1;\n progressBar.update(total);\n return [requestToDownload, localResults];\n },\n { concurrency },\n );\n\n progressBar.stop();\n const t1 = new Date().getTime();\n const totalTime = t1 - t0;\n\n logger.info(\n colors.green(\n `Successfully downloaded file metadata ${requests.length} requests in \"${\n totalTime / 1000\n }\" seconds!`,\n ),\n );\n\n return results;\n}\n","import { map } from '@transcend-io/utils';\nimport colors from 'colors';\nimport type { Got } from 'got';\n\nimport { logger } from '../../logger.js';\nimport { RequestFileMetadata } from './getFileMetadataForPrivacyRequests.js';\n\n/**\n * This function will take in a set of file metadata for privacy requests\n * call the Transcend API to stream the file metadata for these requests\n * and pass that through a callback function\n *\n * @param fileMetadata - Metadata to download\n * @param options - Options for the request\n */\nexport async function streamPrivacyRequestFiles(\n fileMetadata: RequestFileMetadata[],\n {\n requestId,\n sombra,\n onFileDownloaded,\n concurrency = 20,\n }: {\n /** Request ID for logging */\n requestId: string;\n /** Sombra got instance */\n sombra: Got;\n /** Handler on each file */\n onFileDownloaded: (metadata: RequestFileMetadata, stream: Buffer) => void;\n /** Concurrent downloads at once */\n concurrency?: number;\n },\n): Promise<void> {\n // Loop over each file\n await map(\n fileMetadata,\n async (metadata) => {\n try {\n // Construct the stream\n await sombra\n .get('v1/files', {\n searchParams: {\n downloadKey: metadata.downloadKey,\n },\n })\n .buffer()\n .then((fileResponse) => onFileDownloaded(metadata, fileResponse));\n } catch (err) {\n if (err?.response?.body?.includes('fileMetadata#verify')) {\n logger.error(\n colors.red(\n `Failed to pull file for: ${metadata.fileName} (request:${requestId}) - JWT expired. ` +\n 'This likely means that the file is no longer available. ' +\n 'Try restarting the request from scratch in Transcend Admin Dashboard. ' +\n 'Skipping the download of this file.',\n ),\n );\n return;\n }\n throw new Error(`Received an error from server: ${err?.response?.body || err?.message}`);\n }\n },\n {\n concurrency,\n },\n );\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport { RequestAction, RequestStatus } from '@transcend-io/privacy-types';\nimport {\n buildTranscendGraphQLClient,\n createSombraGotInstance,\n makeGraphQLRequest,\n} from '@transcend-io/sdk';\nimport { map } from '@transcend-io/utils';\nimport cliProgress from 'cli-progress';\nimport colors from 'colors';\n\nimport { DEFAULT_TRANSCEND_API } from '../../constants.js';\nimport { logger } from '../../logger.js';\nimport { fetchAllRequests, APPROVE_PRIVACY_REQUEST } from '../graphql/index.js';\nimport { getFileMetadataForPrivacyRequests } from './getFileMetadataForPrivacyRequests.js';\nimport { streamPrivacyRequestFiles } from './streamPrivacyRequestFiles.js';\n\n/**\n * Download a set of privacy requests to disk\n *\n * @param options - Options\n * @returns The number of requests canceled\n */\nexport async function downloadPrivacyRequestFiles({\n auth,\n folderPath,\n requestIds,\n createdAtBefore,\n sombraAuth,\n createdAtAfter,\n updatedAtBefore,\n updatedAtAfter,\n statuses = [RequestStatus.Approving, RequestStatus.Downloadable],\n concurrency = 5,\n transcendUrl = DEFAULT_TRANSCEND_API,\n approveAfterDownload = false,\n}: {\n /** The folder path to download the files to */\n folderPath: string;\n /** Transcend API key authentication */\n auth: string;\n /** Sombra API key authentication */\n sombraAuth?: string;\n /** Concurrency limit for approving */\n concurrency?: number;\n /** The request statuses to cancel */\n statuses?: RequestStatus[];\n /** The set of privacy requests to cancel */\n requestIds?: string[];\n /** Filter for requests created before this date */\n createdAtBefore?: Date;\n /** Filter for requests created after this date */\n createdAtAfter?: Date;\n /** Filter for requests updated before this date */\n updatedAtBefore?: Date;\n /** Filter for requests updated after this date */\n updatedAtAfter?: Date;\n /** API URL for Transcend backend */\n transcendUrl?: string;\n /** When true, approve any requests in Transcend that are in status=APPROVING */\n approveAfterDownload?: boolean;\n}): Promise<number> {\n // Find all requests made before createdAt that are in a removing data state\n const client = buildTranscendGraphQLClient(transcendUrl, auth);\n\n // Create sombra instance to communicate with\n const sombra = await createSombraGotInstance(transcendUrl, auth, {\n logger,\n sombraApiKey: sombraAuth,\n sombraUrl: process.env.SOMBRA_URL,\n });\n\n // Create the folder if it does not exist\n if (!existsSync(folderPath)) {\n mkdirSync(folderPath);\n }\n\n // Pull in the requests\n const allRequests = await fetchAllRequests(client, {\n actions: [RequestAction.Access],\n createdAtBefore,\n createdAtAfter,\n updatedAtBefore,\n updatedAtAfter,\n statuses,\n requestIds,\n });\n\n // Download the file metadata for each request\n const requestFileMetadata = await getFileMetadataForPrivacyRequests(allRequests, {\n sombra,\n concurrency,\n });\n\n // Start timer for download process\n const t0 = new Date().getTime();\n const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);\n let total = 0;\n let totalApproved = 0;\n progressBar.start(allRequests.length, 0);\n\n // Download the files for each request\n await map(\n requestFileMetadata,\n async ([request, metadata]) => {\n // Create a new folder to store request files\n const requestFolder = join(folderPath, request.id);\n if (!existsSync(requestFolder)) {\n mkdirSync(requestFolder);\n }\n\n // Stream each file to disk\n await streamPrivacyRequestFiles(metadata, {\n sombra,\n requestId: request.id,\n onFileDownloaded: (fil, stream) => {\n // Ensure a folder exists for the file\n // filename looks like Health/heartbeat.csv\n const filePath = join(requestFolder, fil.fileName);\n const folder = dirname(filePath);\n if (!existsSync(folder)) {\n mkdirSync(folder, { recursive: true });\n }\n\n // Write to disk\n writeFileSync(filePath, stream);\n },\n });\n\n // Approve the request if requested\n if (approveAfterDownload && request.status === RequestStatus.Approving) {\n await makeGraphQLRequest(client, APPROVE_PRIVACY_REQUEST, {\n variables: { input: { requestId: request.id } },\n logger,\n });\n totalApproved += 1;\n }\n\n // Increment the progress bar\n total += 1;\n progressBar.update(total);\n },\n { concurrency },\n );\n\n progressBar.stop();\n const t1 = new Date().getTime();\n const totalTime = t1 - t0;\n\n logger.info(\n colors.green(`Successfully downloaded ${total} requests in \"${totalTime / 1000}\" seconds!`),\n );\n if (totalApproved > 0) {\n logger.info(colors.green(`Approved ${totalApproved} requests in Transcend.`));\n }\n return allRequests.length;\n}\n"],"mappings":"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"}
|
package/dist/{generateCrossAccountApiKeys-D6hg9146.mjs → generateCrossAccountApiKeys-Cj3YCdZN.mjs}
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as e}from"./constants-
|
|
2
|
-
//# sourceMappingURL=generateCrossAccountApiKeys-
|
|
1
|
+
import{a as e}from"./constants-D22_ckyl.mjs";import{t}from"./logger-Bj782ZYD.mjs";import n from"colors";import{assumeRole as r,buildTranscendGraphQLClientGeneric as i,createApiKey as a,deleteApiKey as o,fetchAllApiKeys as s,loginUser as c}from"@transcend-io/sdk";import{mapSeries as l}from"@transcend-io/utils";async function u({email:u,password:d,scopes:f,apiKeyTitle:p,parentOrganizationId:m,deleteExistingApiKey:h=!0,createNewApiKey:g=!0,transcendUrl:_=e}){let v=await i(_,{});t.info(n.magenta(`Logging in using email and password.`));let{roles:y,loginCookie:b}=await c(v,{email:u,password:d,logger:t});t.info(n.green(`Successfully logged in and found ${y.length} role${y.length===1?``:`s`}!`));let x=m?y.filter(e=>e.organization.id===m||e.organization.parentOrganizationId===m):y;v.setHeaders({Cookie:b});let S=[],C=[];return t.info(n.magenta(`Generating API keys with title: ${p}, scopes: ${f.join(`,`)}.`)),await l(x,async e=>{try{await r(v,{roleId:e.id,email:u,logger:t}),t.info(n.magenta(`Checking if API key already exists in organization "${e.organization.name}" with title: "${p}".`));let[i]=await s(v,{logger:t,filterBy:{titles:[p]}});if(i&&h)t.info(n.yellow(`Deleting existing API key in "${e.organization.name}" with title: "${p}".`)),await o(v,{id:i.id,logger:t}),t.info(n.green(`Successfully deleted API key in "${e.organization.name}" with title: "${p}".`));else if(i)throw Error(`API key already exists with title: "${p}"`);if(g){t.info(n.magenta(`Creating API key in "${e.organization.name}" with title: "${p}".`));let{apiKey:r}=await a(v,{input:{title:p,scopes:f},logger:t});S.push({organizationName:e.organization.name,organizationId:e.organization.id,apiKey:r}),t.info(n.green(`Successfully created API key in "${e.organization.name}" with title: "${p}".`))}else S.push({organizationName:e.organization.name,organizationId:e.organization.id,apiKey:``})}catch(r){t.error(n.red(`Failed to create API key in organization "${e.organization.name}"! - ${r.message}`)),C.push({organizationName:e.organization.name,organizationId:e.organization.id,error:r.message})}}),t.info(n.green(`Successfully created ${S.length} API key${S.length===1?``:`s`}`)),C.length>0&&t.error(n.red(`Failed to create ${C.length} API key${C.length===1?``:`s`}!`)),{errors:C,apiKeys:S}}export{u as t};
|
|
2
|
+
//# sourceMappingURL=generateCrossAccountApiKeys-Cj3YCdZN.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateCrossAccountApiKeys-
|
|
1
|
+
{"version":3,"file":"generateCrossAccountApiKeys-Cj3YCdZN.mjs","names":[],"sources":["../src/lib/api-keys/generateCrossAccountApiKeys.ts"],"sourcesContent":["import { ScopeName } from '@transcend-io/privacy-types';\nimport {\n buildTranscendGraphQLClientGeneric,\n loginUser,\n createApiKey,\n fetchAllApiKeys,\n deleteApiKey,\n assumeRole,\n} from '@transcend-io/sdk';\nimport { mapSeries } from '@transcend-io/utils';\nimport colors from 'colors';\n\nimport { StoredApiKey } from '../../codecs.js';\nimport { DEFAULT_TRANSCEND_API } from '../../constants.js';\nimport { logger } from '../../logger.js';\n\nexport interface ApiKeyGenerateError {\n /** Name of instance */\n organizationName: string;\n /** Error */\n error: string;\n /** Organization ID API key is for */\n organizationId: string;\n}\n\n/**\n * Generate API keys across multiple transcend accounts\n *\n * @param options - Options\n * @returns Number of API keys created\n */\nexport async function generateCrossAccountApiKeys({\n email,\n password,\n scopes,\n apiKeyTitle,\n parentOrganizationId,\n deleteExistingApiKey = true,\n createNewApiKey = true,\n transcendUrl = DEFAULT_TRANSCEND_API,\n}: {\n /** Email address of user generating API keys */\n email: string;\n /** Password of user generating API keys */\n password: string;\n /** Filter for organizations that match this parent organization ID */\n parentOrganizationId?: string;\n /** Title of the API create to create */\n apiKeyTitle: string;\n /** Title of the API create to create */\n scopes: ScopeName[];\n /** API URL for Transcend backend */\n transcendUrl?: string;\n /** When true delete existing API keys with that title, if set to false an API key exists with that title, an error is thrown */\n deleteExistingApiKey?: boolean;\n /** When true, generate new API keys, otherwise only will delete past API keys */\n createNewApiKey?: boolean;\n}): Promise<{\n /** Successfully generated */\n apiKeys: StoredApiKey[];\n /** Error results */\n errors: ApiKeyGenerateError[];\n}> {\n // Create GraphQL client\n const client = await buildTranscendGraphQLClientGeneric(transcendUrl, {});\n\n // Login the user\n logger.info(colors.magenta('Logging in using email and password.'));\n const { roles, loginCookie } = await loginUser(client, { email, password, logger });\n logger.info(\n colors.green(\n `Successfully logged in and found ${roles.length} role${roles.length === 1 ? '' : 's'}!`,\n ),\n );\n\n // Filter down by parentOrganizationId\n const filteredRoles = parentOrganizationId\n ? roles.filter(\n (role) =>\n role.organization.id === parentOrganizationId ||\n role.organization.parentOrganizationId === parentOrganizationId,\n )\n : roles;\n\n // Save cookie to call route subsequent times\n client.setHeaders({\n Cookie: loginCookie,\n });\n\n // Save the resulting API keys\n const results: StoredApiKey[] = [];\n const errors: ApiKeyGenerateError[] = [];\n\n // Generate API keys\n logger.info(\n colors.magenta(`Generating API keys with title: ${apiKeyTitle}, scopes: ${scopes.join(',')}.`),\n );\n\n // Map over each role\n await mapSeries(filteredRoles, async (role) => {\n try {\n // Log into the other instance\n await assumeRole(client, { roleId: role.id, email, logger });\n\n // Grab API keys with that title\n logger.info(\n colors.magenta(\n `Checking if API key already exists in organization \"${role.organization.name}\" with title: \"${apiKeyTitle}\".`,\n ),\n );\n\n // Delete existing API key\n const [apiKeyWithTitle] = await fetchAllApiKeys(client, {\n logger,\n filterBy: { titles: [apiKeyTitle] },\n });\n if (apiKeyWithTitle && deleteExistingApiKey) {\n logger.info(\n colors.yellow(\n `Deleting existing API key in \"${role.organization.name}\" with title: \"${apiKeyTitle}\".`,\n ),\n );\n await deleteApiKey(client, { id: apiKeyWithTitle.id, logger });\n logger.info(\n colors.green(\n `Successfully deleted API key in \"${role.organization.name}\" with title: \"${apiKeyTitle}\".`,\n ),\n );\n } else if (apiKeyWithTitle) {\n // throw error if one exists but not configured to delete\n throw new Error(`API key already exists with title: \"${apiKeyTitle}\"`);\n }\n\n // Create the API key\n if (createNewApiKey) {\n logger.info(\n colors.magenta(\n `Creating API key in \"${role.organization.name}\" with title: \"${apiKeyTitle}\".`,\n ),\n );\n const { apiKey } = await createApiKey(client, {\n input: { title: apiKeyTitle, scopes },\n logger,\n });\n results.push({\n organizationName: role.organization.name,\n organizationId: role.organization.id,\n apiKey,\n });\n logger.info(\n colors.green(\n `Successfully created API key in \"${role.organization.name}\" with title: \"${apiKeyTitle}\".`,\n ),\n );\n } else {\n // Delete only\n results.push({\n organizationName: role.organization.name,\n organizationId: role.organization.id,\n apiKey: '',\n });\n }\n } catch (err) {\n logger.error(\n colors.red(\n `Failed to create API key in organization \"${role.organization.name}\"! - ${err.message}`,\n ),\n );\n errors.push({\n organizationName: role.organization.name,\n organizationId: role.organization.id,\n error: err.message,\n });\n }\n });\n logger.info(\n colors.green(\n `Successfully created ${results.length} API key${results.length === 1 ? '' : 's'}`,\n ),\n );\n\n if (errors.length > 0) {\n logger.error(\n colors.red(`Failed to create ${errors.length} API key${errors.length === 1 ? '' : 's'}!`),\n );\n }\n\n return { errors, apiKeys: results };\n}\n"],"mappings":"uTA+BA,eAAsB,EAA4B,CAChD,QACA,WACA,SACA,cACA,uBACA,uBAAuB,GACvB,kBAAkB,GAClB,eAAe,GAuBd,CAED,IAAM,EAAS,MAAM,EAAmC,EAAc,EAAE,CAAC,CAGzE,EAAO,KAAK,EAAO,QAAQ,uCAAuC,CAAC,CACnE,GAAM,CAAE,QAAO,eAAgB,MAAM,EAAU,EAAQ,CAAE,QAAO,WAAU,SAAQ,CAAC,CACnF,EAAO,KACL,EAAO,MACL,oCAAoC,EAAM,OAAO,OAAO,EAAM,SAAW,EAAI,GAAK,IAAI,GACvF,CACF,CAGD,IAAM,EAAgB,EAClB,EAAM,OACH,GACC,EAAK,aAAa,KAAO,GACzB,EAAK,aAAa,uBAAyB,EAC9C,CACD,EAGJ,EAAO,WAAW,CAChB,OAAQ,EACT,CAAC,CAGF,IAAM,EAA0B,EAAE,CAC5B,EAAgC,EAAE,CAgGxC,OA7FA,EAAO,KACL,EAAO,QAAQ,mCAAmC,EAAY,YAAY,EAAO,KAAK,IAAI,CAAC,GAAG,CAC/F,CAGD,MAAM,EAAU,EAAe,KAAO,IAAS,CAC7C,GAAI,CAEF,MAAM,EAAW,EAAQ,CAAE,OAAQ,EAAK,GAAI,QAAO,SAAQ,CAAC,CAG5D,EAAO,KACL,EAAO,QACL,uDAAuD,EAAK,aAAa,KAAK,iBAAiB,EAAY,IAC5G,CACF,CAGD,GAAM,CAAC,GAAmB,MAAM,EAAgB,EAAQ,CACtD,SACA,SAAU,CAAE,OAAQ,CAAC,EAAY,CAAE,CACpC,CAAC,CACF,GAAI,GAAmB,EACrB,EAAO,KACL,EAAO,OACL,iCAAiC,EAAK,aAAa,KAAK,iBAAiB,EAAY,IACtF,CACF,CACD,MAAM,EAAa,EAAQ,CAAE,GAAI,EAAgB,GAAI,SAAQ,CAAC,CAC9D,EAAO,KACL,EAAO,MACL,oCAAoC,EAAK,aAAa,KAAK,iBAAiB,EAAY,IACzF,CACF,SACQ,EAET,MAAU,MAAM,uCAAuC,EAAY,GAAG,CAIxE,GAAI,EAAiB,CACnB,EAAO,KACL,EAAO,QACL,wBAAwB,EAAK,aAAa,KAAK,iBAAiB,EAAY,IAC7E,CACF,CACD,GAAM,CAAE,UAAW,MAAM,EAAa,EAAQ,CAC5C,MAAO,CAAE,MAAO,EAAa,SAAQ,CACrC,SACD,CAAC,CACF,EAAQ,KAAK,CACX,iBAAkB,EAAK,aAAa,KACpC,eAAgB,EAAK,aAAa,GAClC,SACD,CAAC,CACF,EAAO,KACL,EAAO,MACL,oCAAoC,EAAK,aAAa,KAAK,iBAAiB,EAAY,IACzF,CACF,MAGD,EAAQ,KAAK,CACX,iBAAkB,EAAK,aAAa,KACpC,eAAgB,EAAK,aAAa,GAClC,OAAQ,GACT,CAAC,OAEG,EAAK,CACZ,EAAO,MACL,EAAO,IACL,6CAA6C,EAAK,aAAa,KAAK,OAAO,EAAI,UAChF,CACF,CACD,EAAO,KAAK,CACV,iBAAkB,EAAK,aAAa,KACpC,eAAgB,EAAK,aAAa,GAClC,MAAO,EAAI,QACZ,CAAC,GAEJ,CACF,EAAO,KACL,EAAO,MACL,wBAAwB,EAAQ,OAAO,UAAU,EAAQ,SAAW,EAAI,GAAK,MAC9E,CACF,CAEG,EAAO,OAAS,GAClB,EAAO,MACL,EAAO,IAAI,oBAAoB,EAAO,OAAO,UAAU,EAAO,SAAW,EAAI,GAAK,IAAI,GAAG,CAC1F,CAGI,CAAE,SAAQ,QAAS,EAAS"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./logger-Bj782ZYD.mjs";import{t}from"./collectCsvFilesOrExit-CbtyKAzu.mjs";import{t as n}from"./done-input-validation-BcNBxhEs.mjs";import{a as r,i,n as a,r as o,t as s}from"./createExtraKeyHandler-
|
|
2
|
-
//# sourceMappingURL=impl-
|
|
1
|
+
import{t as e}from"./logger-Bj782ZYD.mjs";import{t}from"./collectCsvFilesOrExit-CbtyKAzu.mjs";import{t as n}from"./done-input-validation-BcNBxhEs.mjs";import{a as r,i,n as a,r as o,t as s}from"./createExtraKeyHandler-BkfSV_aF.mjs";import c from"colors";import{CHILD_FLAG as l,PoolCancelledError as u,computePoolSize as d,resolveWorkerPath as f,runPool as p}from"@transcend-io/utils";function m(e){return a(e)}function h(e){return o(e)}const g={renderHeader:m,renderWorkers:h};async function _(a){n(this.process.exit);let{directory:o,outputDir:m,clearOutputDir:h,chunkSizeMB:_,concurrency:v,viewerMode:y}=a,b=t(o,this),{poolSize:x,cpuCount:S}=d(v,b.length);e.info(c.green(`Chunking ${b.length} CSV file(s) with pool size ${x} (CPU=${S})`));let C=b.map(e=>({filePath:e,options:{outputDir:m,clearOutputDir:h,chunkSizeMB:_}}));await p({title:`Chunk CSV - ${o}`,baseDir:o||m||process.cwd(),childFlag:l,childModulePath:f(import.meta.url,`commands/admin/chunk-csv/worker.mjs`),poolSize:x,cpuCount:S,filesTotal:b.length,hooks:{nextTask:()=>C.shift(),taskLabel:e=>e.filePath,initTotals:()=>({}),initSlotProgress:()=>void 0,onProgress:e=>e,onResult:(e,t)=>({totals:e,ok:!!t.ok}),postProcess:async()=>{}},viewerMode:y,render:e=>i(e,g,y),installInteractiveSwitcher:y?void 0:({workers:e,onCtrlC:t,getLogPaths:n,replayBytes:i,replayWhich:a,setPaused:o,repaint:s})=>r({workers:e,onCtrlC:t,getLogPaths:n,replayBytes:i,replayWhich:a,onAttach:()=>o(!0),onDetach:()=>{o(!1),s()},onEnterAttachScreen:e=>{o(!0),process.stdout.write(`\x1B[2J\x1B[H`),process.stdout.write(`Attached to worker ${e}. (Esc/Ctrl+] detach \u2022 Ctrl+D EOF \u2022 Ctrl+C SIGINT)\n`)}}),extraKeyHandler:({logsBySlot:e,repaint:t,setPaused:n})=>s({logsBySlot:e,repaint:t,setPaused:n})}).catch(e=>{throw e instanceof u&&process.exit(130),e})}export{_ as chunkCsv};
|
|
2
|
+
//# sourceMappingURL=impl-0w7yd5pK.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impl-
|
|
1
|
+
{"version":3,"file":"impl-0w7yd5pK.mjs","names":[],"sources":["../src/commands/admin/chunk-csv/ui/plugin.ts","../src/commands/admin/chunk-csv/impl.ts"],"sourcesContent":["import {\n makeHeader,\n makeWorkerRows,\n type ChunkSlotProgress,\n type CommonCtx,\n type DashboardPlugin,\n} from '../../../../lib/pooling/index.js';\n\n/**\n * Header for chunk-csv (no extra totals block).\n *\n * @param ctx - Dashboard context.\n * @returns Header lines.\n */\nfunction renderHeader<TTotals>(ctx: CommonCtx<TTotals, ChunkSlotProgress>): string[] {\n // no extra lines — reuse the shared header as-is\n return makeHeader(ctx);\n}\n\n/**\n * Worker rows for chunk-csv — share the generic row renderer.\n *\n * @param ctx - Dashboard context.\n * @returns Array of strings, each representing one worker row.\n */\nfunction renderWorkers<TTotals>(ctx: CommonCtx<TTotals, ChunkSlotProgress>): string[] {\n return makeWorkerRows(ctx);\n}\n\nexport const chunkCsvPlugin: DashboardPlugin<unknown, ChunkSlotProgress> = {\n renderHeader,\n renderWorkers,\n // no extras\n};\n","import {\n CHILD_FLAG,\n type PoolHooks,\n runPool,\n computePoolSize,\n PoolCancelledError,\n resolveWorkerPath,\n} from '@transcend-io/utils';\nimport colors from 'colors';\n\nimport type { LocalContext } from '../../../context.js';\nimport { doneInputValidation } from '../../../lib/cli/done-input-validation.js';\nimport { collectCsvFilesOrExit } from '../../../lib/helpers/collectCsvFilesOrExit.js';\nimport {\n createExtraKeyHandler,\n dashboardPlugin,\n installInteractiveSwitcher,\n} from '../../../lib/pooling/index.js';\nimport { logger } from '../../../logger.js';\nimport { chunkCsvPlugin } from './ui/index.js';\nimport type { ChunkProgress, ChunkResult, ChunkTask } from './worker.js';\n\n/**\n * Totals aggregate for this command.\n * We don’t need custom counters since the runner already tracks\n * completed/failed counts in its header — so we just use an empty record.\n */\ntype Totals = Record<string, never>;\n\n/**\n * CLI flags accepted by the `chunk-csv` command.\n *\n * These are passed down from the CLI parser into the parent process.\n */\nexport type ChunkCsvCommandFlags = {\n directory: string;\n outputDir?: string;\n clearOutputDir: boolean;\n chunkSizeMB: number;\n concurrency?: number;\n viewerMode: boolean;\n};\n\n/**\n * Parent entrypoint for chunking many CSVs in parallel using the worker pool runner.\n *\n * Lifecycle:\n * 1) Discover CSV inputs (exit if none).\n * 2) Compute pool size (CPU-count heuristic or --concurrency).\n * 3) Build a FIFO queue of `ChunkTask`s.\n * 4) Define pool hooks to drive task assignment, progress, and result handling.\n * 5) Launch the pool with `runPool`, rendering via the `chunkCsvPlugin`.\n *\n * @param this - Bound CLI context (provides process exit + logging).\n * @param flags - CLI options for the run.\n */\nexport async function chunkCsv(this: LocalContext, flags: ChunkCsvCommandFlags): Promise<void> {\n doneInputValidation(this.process.exit);\n\n const { directory, outputDir, clearOutputDir, chunkSizeMB, concurrency, viewerMode } = flags;\n\n /* 1) Discover CSV inputs */\n const files = collectCsvFilesOrExit(directory, this);\n\n /* 2) Size the pool */\n const { poolSize, cpuCount } = computePoolSize(concurrency, files.length);\n\n logger.info(\n colors.green(\n `Chunking ${files.length} CSV file(s) with pool size ${poolSize} (CPU=${cpuCount})`,\n ),\n );\n\n /* 3) Prepare a simple FIFO queue of tasks (one per file). */\n const queue = files.map<ChunkTask>((filePath) => ({\n filePath,\n options: { outputDir, clearOutputDir, chunkSizeMB },\n }));\n\n /* 4) Define pool hooks to adapt runner to this command. */\n const hooks: PoolHooks<ChunkTask, ChunkProgress, ChunkResult, Totals> = {\n nextTask: () => queue.shift(),\n taskLabel: (t) => t.filePath,\n initTotals: () => ({}) as Totals,\n initSlotProgress: () => undefined,\n onProgress: (totals) => totals,\n onResult: (totals, res) => ({ totals, ok: !!res.ok }),\n // postProcess receives log context when viewerMode=true — we don’t need it here.\n postProcess: async () => {\n // nothing extra for chunk-csv\n },\n };\n\n /* 5) Launch the pool runner with our hooks and custom dashboard plugin. */\n await runPool({\n title: `Chunk CSV - ${directory}`,\n baseDir: directory || outputDir || process.cwd(),\n childFlag: CHILD_FLAG,\n childModulePath: resolveWorkerPath(import.meta.url, 'commands/admin/chunk-csv/worker.mjs'),\n poolSize,\n cpuCount,\n filesTotal: files.length,\n hooks,\n viewerMode,\n render: (input) => dashboardPlugin(input, chunkCsvPlugin, viewerMode),\n installInteractiveSwitcher: viewerMode\n ? undefined\n : ({\n workers,\n onCtrlC,\n getLogPaths,\n replayBytes: rb,\n replayWhich: rw,\n setPaused,\n repaint: rp,\n }) =>\n installInteractiveSwitcher({\n workers,\n onCtrlC,\n getLogPaths,\n replayBytes: rb,\n replayWhich: rw,\n onAttach: () => setPaused(true),\n onDetach: () => {\n setPaused(false);\n rp();\n },\n onEnterAttachScreen: (id) => {\n setPaused(true);\n process.stdout.write('\\x1b[2J\\x1b[H');\n process.stdout.write(\n `Attached to worker ${id}. (Esc/Ctrl+] detach \\u2022 Ctrl+D EOF \\u2022 Ctrl+C SIGINT)\\n`,\n );\n },\n }),\n extraKeyHandler: ({ logsBySlot, repaint, setPaused }) =>\n createExtraKeyHandler({\n logsBySlot,\n repaint,\n setPaused,\n }),\n }).catch((err) => {\n if (err instanceof PoolCancelledError) {\n process.exit(130);\n }\n throw err;\n });\n}\n"],"mappings":"+XAcA,SAAS,EAAsB,EAAsD,CAEnF,OAAO,EAAW,EAAI,CASxB,SAAS,EAAuB,EAAsD,CACpF,OAAO,EAAe,EAAI,CAG5B,MAAa,EAA8D,CACzE,eACA,gBAED,CCuBD,eAAsB,EAA6B,EAA4C,CAC7F,EAAoB,KAAK,QAAQ,KAAK,CAEtC,GAAM,CAAE,YAAW,YAAW,iBAAgB,cAAa,cAAa,cAAe,EAGjF,EAAQ,EAAsB,EAAW,KAAK,CAG9C,CAAE,WAAU,YAAa,EAAgB,EAAa,EAAM,OAAO,CAEzE,EAAO,KACL,EAAO,MACL,YAAY,EAAM,OAAO,8BAA8B,EAAS,QAAQ,EAAS,GAClF,CACF,CAGD,IAAM,EAAQ,EAAM,IAAgB,IAAc,CAChD,WACA,QAAS,CAAE,YAAW,iBAAgB,cAAa,CACpD,EAAE,CAiBH,MAAM,EAAQ,CACZ,MAAO,eAAe,IACtB,QAAS,GAAa,GAAa,QAAQ,KAAK,CAChD,UAAW,EACX,gBAAiB,EAAkB,OAAO,KAAK,IAAK,sCAAsC,CAC1F,WACA,WACA,WAAY,EAAM,OAClB,MAtBsE,CACtE,aAAgB,EAAM,OAAO,CAC7B,UAAY,GAAM,EAAE,SACpB,gBAAmB,EAAE,EACrB,qBAAwB,IAAA,GACxB,WAAa,GAAW,EACxB,UAAW,EAAQ,KAAS,CAAE,SAAQ,GAAI,CAAC,CAAC,EAAI,GAAI,EAEpD,YAAa,SAAY,GAG1B,CAYC,aACA,OAAS,GAAU,EAAgB,EAAO,EAAgB,EAAW,CACrE,2BAA4B,EACxB,IAAA,IACC,CACC,UACA,UACA,cACA,YAAa,EACb,YAAa,EACb,YACA,QAAS,KAET,EAA2B,CACzB,UACA,UACA,cACA,YAAa,EACb,YAAa,EACb,aAAgB,EAAU,GAAK,CAC/B,aAAgB,CACd,EAAU,GAAM,CAChB,GAAI,EAEN,oBAAsB,GAAO,CAC3B,EAAU,GAAK,CACf,QAAQ,OAAO,MAAM,gBAAgB,CACrC,QAAQ,OAAO,MACb,sBAAsB,EAAG,gEAC1B,EAEJ,CAAC,CACR,iBAAkB,CAAE,aAAY,UAAS,eACvC,EAAsB,CACpB,aACA,UACA,YACD,CAAC,CACL,CAAC,CAAC,MAAO,GAAQ,CAIhB,MAHI,aAAe,GACjB,QAAQ,KAAK,IAAI,CAEb,GACN"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./logger-Bj782ZYD.mjs";import{t}from"./streamPrivacyRequestsToCsv-
|
|
2
|
-
//# sourceMappingURL=impl-
|
|
1
|
+
import{t as e}from"./logger-Bj782ZYD.mjs";import{t}from"./streamPrivacyRequestsToCsv-CipyYYbS.mjs";import{t as n}from"./done-input-validation-BcNBxhEs.mjs";import r from"colors";async function i({auth:i,transcendUrl:a,file:o,pageLimit:s,concurrency:c,actions:l,sombraAuth:u,skipRequestIdentifiers:d,statuses:f,createdAtBefore:p,createdAtAfter:m,updatedAtBefore:h,updatedAtAfter:g,showTests:_}){n(this.process.exit);let{filePaths:v,totalCount:y}=await t({transcendUrl:a,concurrency:c,pageLimit:s,actions:l,statuses:f,auth:i,sombraAuth:u,skipRequestIdentifiers:d,createdAtBefore:p,createdAtAfter:m,updatedAtBefore:h,updatedAtAfter:g,isTest:_,file:o});e.info(r.green(`Successfully wrote ${y} requests to ${v.length} file(s): ${v.join(`, `)}`))}export{i as _export};
|
|
2
|
+
//# sourceMappingURL=impl-122G24x5.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impl-
|
|
1
|
+
{"version":3,"file":"impl-122G24x5.mjs","names":[],"sources":["../src/commands/request/export/impl.ts"],"sourcesContent":["import type { RequestAction, RequestStatus } from '@transcend-io/privacy-types';\nimport colors from 'colors';\n\nimport type { LocalContext } from '../../../context.js';\nimport { doneInputValidation } from '../../../lib/cli/done-input-validation.js';\nimport { streamPrivacyRequestsToCsv } from '../../../lib/requests/index.js';\nimport { logger } from '../../../logger.js';\n\nexport interface ExportCommandFlags {\n auth: string;\n sombraAuth?: string;\n actions?: RequestAction[];\n statuses?: RequestStatus[];\n transcendUrl: string;\n file: string;\n concurrency: number;\n createdAtBefore?: Date;\n createdAtAfter?: Date;\n updatedAtBefore?: Date;\n updatedAtAfter?: Date;\n showTests?: boolean;\n skipRequestIdentifiers?: boolean;\n pageLimit: number;\n}\n\n// `export` is a reserved keyword, so we need to prefix it with an underscore\n// eslint-disable-next-line no-underscore-dangle\nexport async function _export(\n this: LocalContext,\n {\n auth,\n transcendUrl,\n file,\n pageLimit,\n concurrency,\n actions,\n sombraAuth,\n skipRequestIdentifiers,\n statuses,\n createdAtBefore,\n createdAtAfter,\n updatedAtBefore,\n updatedAtAfter,\n showTests,\n }: ExportCommandFlags,\n): Promise<void> {\n doneInputValidation(this.process.exit);\n\n const { filePaths, totalCount } = await streamPrivacyRequestsToCsv({\n transcendUrl,\n concurrency,\n pageLimit,\n actions,\n statuses,\n auth,\n sombraAuth,\n skipRequestIdentifiers,\n createdAtBefore,\n createdAtAfter,\n updatedAtBefore,\n updatedAtAfter,\n isTest: showTests,\n file,\n });\n\n logger.info(\n colors.green(\n `Successfully wrote ${totalCount} requests to ` +\n `${filePaths.length} file(s): ${filePaths.join(', ')}`,\n ),\n );\n}\n"],"mappings":"kLA2BA,eAAsB,EAEpB,CACE,OACA,eACA,OACA,YACA,cACA,UACA,aACA,yBACA,WACA,kBACA,iBACA,kBACA,iBACA,aAEa,CACf,EAAoB,KAAK,QAAQ,KAAK,CAEtC,GAAM,CAAE,YAAW,cAAe,MAAM,EAA2B,CACjE,eACA,cACA,YACA,UACA,WACA,OACA,aACA,yBACA,kBACA,iBACA,kBACA,iBACA,OAAQ,EACR,OACD,CAAC,CAEF,EAAO,KACL,EAAO,MACL,sBAAsB,EAAW,eAC5B,EAAU,OAAO,YAAY,EAAU,KAAK,KAAK,GACvD,CACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{n as e}from"./constants-
|
|
2
|
-
//# sourceMappingURL=impl
|
|
1
|
+
import{n as e}from"./constants-D22_ckyl.mjs";import{t}from"./logger-Bj782ZYD.mjs";import{s as n}from"./writeCsv-C4pjXGsD.mjs";import{t as r}from"./pullAllDatapoints-Bbmky50p.mjs";import{t as i}from"./done-input-validation-BcNBxhEs.mjs";import{groupBy as a,uniq as o}from"lodash-es";import s from"colors";import{buildTranscendGraphQLClient as c}from"@transcend-io/sdk";async function l({auth:l,file:u,transcendUrl:d,dataSiloIds:f,includeAttributes:p,includeGuessedCategories:m,parentCategories:h,subCategories:g=[]}){i(this.process.exit);try{let e=await r(c(d,l),{dataSiloIds:f,includeGuessedCategories:m,parentCategories:h,includeAttributes:p,subCategories:g});t.info(s.magenta(`Writing datapoints to file "${u}"...`));let i=[];await n(u,e.map(e=>{let t={"Property ID":e.id,"Data Silo":e.dataSilo.title,Object:e.dataPoint.name,"Object Path":e.dataPoint.path.join(`.`),Property:e.name,"Property Description":e.description,"Data Categories":e.categories.map(e=>`${e.category}:${e.name}`).join(`, `),"Guessed Category":e.pendingCategoryGuesses?.[0]?`${e.pendingCategoryGuesses[0].category.category}:${e.pendingCategoryGuesses[0].category.name}`:``,"Processing Purposes":e.purposes.map(e=>`${e.purpose}:${e.name}`).join(`, `),...Object.entries(a(e.attributeValues||[],({attributeKey:e})=>e.name)).reduce((e,[t,n])=>(e[t]=n.map(e=>e.name).join(`,`),e),{})};return i=o([...i,...Object.keys(t)]),t}),i)}catch(e){t.error(s.red(`An error occurred syncing the datapoints: ${e.message}`)),this.process.exit(1)}t.info(s.green(`Successfully synced datapoints to disk at ${u}! View at ${e}`))}export{l as pullDatapoints};
|
|
2
|
+
//# sourceMappingURL=impl-56wfH4jn.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impl
|
|
1
|
+
{"version":3,"file":"impl-56wfH4jn.mjs","names":[],"sources":["../src/commands/inventory/pull-datapoints/impl.ts"],"sourcesContent":["import { DataCategoryType } from '@transcend-io/privacy-types';\nimport { buildTranscendGraphQLClient } from '@transcend-io/sdk';\nimport colors from 'colors';\nimport { uniq, groupBy } from 'lodash-es';\n\nimport { ADMIN_DASH_DATAPOINTS } from '../../../constants.js';\nimport type { LocalContext } from '../../../context.js';\nimport { doneInputValidation } from '../../../lib/cli/done-input-validation.js';\nimport { pullAllDatapoints } from '../../../lib/data-inventory/index.js';\nimport { writeLargeCsv } from '../../../lib/helpers/index.js';\nimport { logger } from '../../../logger.js';\n\nexport interface PullDatapointsCommandFlags {\n auth: string;\n file: string;\n transcendUrl: string;\n dataSiloIds?: string[];\n includeAttributes: boolean;\n includeGuessedCategories: boolean;\n parentCategories?: DataCategoryType[];\n subCategories?: string[];\n}\n\nexport async function pullDatapoints(\n this: LocalContext,\n {\n auth,\n file,\n transcendUrl,\n dataSiloIds,\n includeAttributes,\n includeGuessedCategories,\n parentCategories,\n subCategories = [],\n }: PullDatapointsCommandFlags,\n): Promise<void> {\n doneInputValidation(this.process.exit);\n\n try {\n // Create a GraphQL client\n const client = buildTranscendGraphQLClient(transcendUrl, auth);\n\n const dataPoints = await pullAllDatapoints(client, {\n dataSiloIds,\n includeGuessedCategories,\n parentCategories,\n includeAttributes,\n subCategories, // TODO: https://transcend.height.app/T-40482 - do by name not ID\n });\n\n logger.info(colors.magenta(`Writing datapoints to file \"${file}\"...`));\n let headers: string[] = [];\n const inputs = dataPoints.map((point) => {\n const result = {\n 'Property ID': point.id,\n 'Data Silo': point.dataSilo.title,\n Object: point.dataPoint.name,\n 'Object Path': point.dataPoint.path.join('.'),\n Property: point.name,\n 'Property Description': point.description,\n 'Data Categories': point.categories\n .map((category) => `${category.category}:${category.name}`)\n .join(', '),\n 'Guessed Category': point.pendingCategoryGuesses?.[0]\n ? `${point.pendingCategoryGuesses![0]!.category.category}:${\n point.pendingCategoryGuesses![0]!.category.name\n }`\n : '',\n 'Processing Purposes': point.purposes\n .map((purpose) => `${purpose.purpose}:${purpose.name}`)\n .join(', '),\n ...Object.entries(\n groupBy(point.attributeValues || [], ({ attributeKey }) => attributeKey.name),\n ).reduce(\n (acc, [key, values]) => {\n acc[key] = values.map((value) => value.name).join(',');\n return acc;\n },\n {} as Record<string, string>,\n ),\n };\n headers = uniq([...headers, ...Object.keys(result)]);\n return result;\n });\n await writeLargeCsv(file, inputs, headers);\n } catch (err) {\n logger.error(colors.red(`An error occurred syncing the datapoints: ${err.message}`));\n this.process.exit(1);\n }\n\n // Indicate success\n logger.info(\n colors.green(\n `Successfully synced datapoints to disk at ${file}! View at ${ADMIN_DASH_DATAPOINTS}`,\n ),\n );\n}\n"],"mappings":"gXAuBA,eAAsB,EAEpB,CACE,OACA,OACA,eACA,cACA,oBACA,2BACA,mBACA,gBAAgB,EAAE,EAEL,CACf,EAAoB,KAAK,QAAQ,KAAK,CAEtC,GAAI,CAIF,IAAM,EAAa,MAAM,EAFV,EAA4B,EAAc,EAAK,CAEX,CACjD,cACA,2BACA,mBACA,oBACA,gBACD,CAAC,CAEF,EAAO,KAAK,EAAO,QAAQ,+BAA+B,EAAK,MAAM,CAAC,CACtE,IAAI,EAAoB,EAAE,CAiC1B,MAAM,EAAc,EAhCL,EAAW,IAAK,GAAU,CACvC,IAAM,EAAS,CACb,cAAe,EAAM,GACrB,YAAa,EAAM,SAAS,MAC5B,OAAQ,EAAM,UAAU,KACxB,cAAe,EAAM,UAAU,KAAK,KAAK,IAAI,CAC7C,SAAU,EAAM,KAChB,uBAAwB,EAAM,YAC9B,kBAAmB,EAAM,WACtB,IAAK,GAAa,GAAG,EAAS,SAAS,GAAG,EAAS,OAAO,CAC1D,KAAK,KAAK,CACb,mBAAoB,EAAM,yBAAyB,GAC/C,GAAG,EAAM,uBAAwB,GAAI,SAAS,SAAS,GACrD,EAAM,uBAAwB,GAAI,SAAS,OAE7C,GACJ,sBAAuB,EAAM,SAC1B,IAAK,GAAY,GAAG,EAAQ,QAAQ,GAAG,EAAQ,OAAO,CACtD,KAAK,KAAK,CACb,GAAG,OAAO,QACR,EAAQ,EAAM,iBAAmB,EAAE,EAAG,CAAE,kBAAmB,EAAa,KAAK,CAC9E,CAAC,QACC,EAAK,CAAC,EAAK,MACV,EAAI,GAAO,EAAO,IAAK,GAAU,EAAM,KAAK,CAAC,KAAK,IAAI,CAC/C,GAET,EAAE,CACH,CACF,CAED,MADA,GAAU,EAAK,CAAC,GAAG,EAAS,GAAG,OAAO,KAAK,EAAO,CAAC,CAAC,CAC7C,GACP,CACgC,EAAQ,OACnC,EAAK,CACZ,EAAO,MAAM,EAAO,IAAI,6CAA6C,EAAI,UAAU,CAAC,CACpF,KAAK,QAAQ,KAAK,EAAE,CAItB,EAAO,KACL,EAAO,MACL,6CAA6C,EAAK,YAAY,IAC/D,CACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./pushCronIdentifiersFromCsv-
|
|
2
|
-
//# sourceMappingURL=impl-
|
|
1
|
+
import{t as e}from"./pushCronIdentifiersFromCsv-DJywyHYU.mjs";import{t}from"./done-input-validation-BcNBxhEs.mjs";async function n({file:n,transcendUrl:r,auth:i,sombraAuth:a,dataSiloId:o}){t(this.process.exit),await e({file:n,transcendUrl:r,auth:i,sombraAuth:a,dataSiloId:o})}export{n as markIdentifiersCompleted};
|
|
2
|
+
//# sourceMappingURL=impl-7rVYG2LQ.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impl-
|
|
1
|
+
{"version":3,"file":"impl-7rVYG2LQ.mjs","names":[],"sources":["../src/commands/request/cron/mark-identifiers-completed/impl.ts"],"sourcesContent":["import type { LocalContext } from '../../../../context.js';\nimport { doneInputValidation } from '../../../../lib/cli/done-input-validation.js';\nimport { pushCronIdentifiersFromCsv } from '../../../../lib/cron/index.js';\n\nexport interface MarkIdentifiersCompletedCommandFlags {\n file: string;\n transcendUrl: string;\n auth: string;\n sombraAuth?: string;\n dataSiloId: string;\n}\n\nexport async function markIdentifiersCompleted(\n this: LocalContext,\n { file, transcendUrl, auth, sombraAuth, dataSiloId }: MarkIdentifiersCompletedCommandFlags,\n): Promise<void> {\n doneInputValidation(this.process.exit);\n\n await pushCronIdentifiersFromCsv({\n file,\n transcendUrl,\n auth,\n sombraAuth,\n dataSiloId,\n });\n}\n"],"mappings":"kHAYA,eAAsB,EAEpB,CAAE,OAAM,eAAc,OAAM,aAAY,cACzB,CACf,EAAoB,KAAK,QAAQ,KAAK,CAEtC,MAAM,EAA2B,CAC/B,OACA,eACA,OACA,aACA,aACD,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{o as e}from"./enums-CyFTrzXY.mjs";import{r as t}from"./constants-
|
|
2
|
-
//# sourceMappingURL=impl-
|
|
1
|
+
import{o as e}from"./enums-CyFTrzXY.mjs";import{r as t}from"./constants-D22_ckyl.mjs";import{n,t as r}from"./command-CuxgABlk.mjs";import{t as i}from"./logger-Bj782ZYD.mjs";import{a}from"./readTranscendYaml-DVkQL2SC.mjs";import{n as o}from"./pullTranscendConfiguration-DjOELnPo.mjs";import{t as s}from"./validateTranscendAuth-Cuh2Qfdl.mjs";import{t as c}from"./done-input-validation-BcNBxhEs.mjs";import l from"node:fs";import{join as u}from"node:path";import d from"colors";import{buildTranscendGraphQLClient as f}from"@transcend-io/sdk";import{mapSeries as p}from"@transcend-io/utils";async function m({auth:m,resources:h=n,file:g,transcendUrl:_,dataSiloIds:v=[],integrationNames:y=[],trackerStatuses:b=r,pageSize:x,skipDatapoints:S,skipSubDatapoints:C,includeGuessedCategories:w,debug:T}){c(this.process.exit);let E=await s(m),D=h.includes(`all`)?Object.values(e):h;if(typeof E==`string`){try{let e=await o(f(_,E),{dataSiloIds:v,integrationNames:y,resources:D,pageSize:x,debug:T,skipDatapoints:S,skipSubDatapoints:C,includeGuessedCategories:w,trackerStatuses:b});i.info(d.magenta(`Writing configuration to file "${g}"...`)),a(g,e)}catch(e){i.error(d.red(`An error occurred syncing the schema: ${T?e.stack:e.message}`)),this.process.exit(1)}i.info(d.green(`Successfully synced yaml file to disk at ${g}! View at ${t}`))}else{if(!l.lstatSync(g).isDirectory())throw Error(`File is expected to be a folder when passing in a list of API keys to pull from. e.g. --file=./working/`);let e=[];await p(E,async(t,n)=>{let r=`[${n+1}/${E.length}][${t.organizationName}] `;i.info(d.magenta(`~~~\n\n${r}Attempting to pull configuration...\n\n~~~`));let s=f(_,t.apiKey);try{let e=await o(s,{dataSiloIds:v,integrationNames:y,resources:D,pageSize:x,debug:T,skipDatapoints:S,skipSubDatapoints:C,includeGuessedCategories:w,trackerStatuses:b}),n=u(g,`${t.organizationName}.yml`);i.info(d.magenta(`Writing configuration to file "${n}"...`)),a(n,e),i.info(d.green(`${r}Successfully pulled configuration!`))}catch(n){i.error(d.red(`${r}Failed to sync configuration. - ${n.message}`)),e.push(t.organizationName)}}),e.length>0&&(i.info(d.red(`Sync encountered errors for "${e.join(`,`)}". View output above for more information, or check out ${t}`)),this.process.exit(1))}}export{m as pull};
|
|
2
|
+
//# sourceMappingURL=impl-B1YGN9Iu.mjs.map
|