From da856152c7da28405fa40aaadc6dbf9daf8963d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Tue, 9 Jul 2024 16:13:46 +0200 Subject: [PATCH] wip importer --- apps/api/package.json | 1 + apps/api/src/controllers/import.controller.ts | 39 +++ apps/api/src/routes/import.router.ts | 38 +++ apps/api/src/utils/auth.ts | 30 +++ packages/cli/package.json | 41 +++ packages/cli/src/cli.ts | 26 ++ packages/cli/src/importer/index.ts | 199 ++++++++++++++ packages/cli/src/importer/load.ts | 255 ++++++++++++++++++ packages/cli/tsconfig.json | 8 + packages/cli/tsup.config.ts | 9 + packages/sdks/web/cdn.ts | 4 +- pnpm-lock.yaml | 181 ++++++++++++- 12 files changed, 829 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/controllers/import.controller.ts create mode 100644 apps/api/src/routes/import.router.ts create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/cli.ts create mode 100644 packages/cli/src/importer/index.ts create mode 100644 packages/cli/src/importer/load.ts create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/cli/tsup.config.ts diff --git a/apps/api/package.json b/apps/api/package.json index 7f5b1e59..311bbd28 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -38,6 +38,7 @@ "svix": "^1.24.0", "ua-parser-js": "^1.0.37", "url-metadata": "^4.1.0", + "uuid": "^9.0.1", "zod": "^3.22.4" }, "devDependencies": { diff --git a/apps/api/src/controllers/import.controller.ts b/apps/api/src/controllers/import.controller.ts new file mode 100644 index 00000000..17d393ac --- /dev/null +++ b/apps/api/src/controllers/import.controller.ts @@ -0,0 +1,39 @@ +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { pathOr } from 'ramda'; +import { v4 as uuid } from 'uuid'; + +import { toDots } from '@openpanel/common'; +import type { + IClickhouseEvent, + IServiceCreateEventPayload, +} from '@openpanel/db'; +import { ch, formatClickhouseDate } from '@openpanel/db'; +import type { PostEventPayload } from '@openpanel/sdk'; + +export async function importEvents( + request: FastifyRequest<{ + Body: IClickhouseEvent[]; + }>, + reply: FastifyReply +) { + console.log('HERE?!', request.body.length); + + const values: IClickhouseEvent[] = request.body.map((event) => { + return { + ...event, + project_id: request.client?.projectId ?? '', + created_at: formatClickhouseDate(event.created_at), + }; + }); + + const res = await ch.insert({ + table: 'events', + values, + format: 'JSONEachRow', + clickhouse_settings: { + date_time_input_format: 'best_effort', + }, + }); + + reply.send('OK'); +} diff --git a/apps/api/src/routes/import.router.ts b/apps/api/src/routes/import.router.ts new file mode 100644 index 00000000..e993a591 --- /dev/null +++ b/apps/api/src/routes/import.router.ts @@ -0,0 +1,38 @@ +import * as controller from '@/controllers/import.controller'; +import { validateImportRequest } from '@/utils/auth'; +import type { FastifyPluginCallback, FastifyRequest } from 'fastify'; + +import { Prisma } from '@openpanel/db'; + +const importRouter: FastifyPluginCallback = (fastify, opts, done) => { + fastify.addHook('preHandler', async (req: FastifyRequest, reply) => { + try { + const client = await validateImportRequest(req.headers); + req.client = client; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + return reply.status(401).send({ + error: 'Unauthorized', + message: 'Client ID seems to be malformed', + }); + } else if (e instanceof Error) { + return reply + .status(401) + .send({ error: 'Unauthorized', message: e.message }); + } + return reply + .status(401) + .send({ error: 'Unauthorized', message: 'Unexpected error' }); + } + }); + + fastify.route({ + method: 'POST', + url: '/events', + handler: controller.importEvents, + }); + + done(); +}; + +export default importRouter; diff --git a/apps/api/src/utils/auth.ts b/apps/api/src/utils/auth.ts index 43fd9b38..aaabd663 100644 --- a/apps/api/src/utils/auth.ts +++ b/apps/api/src/utils/auth.ts @@ -128,6 +128,36 @@ export async function validateExportRequest( return client; } +export async function validateImportRequest( + headers: RawRequestDefaultExpression['headers'] +): Promise { + const clientId = headers['openpanel-client-id'] as string; + const clientSecret = (headers['openpanel-client-secret'] as string) || ''; + const client = await db.client.findUnique({ + where: { + id: clientId, + }, + }); + + if (!client) { + throw new Error('Import: Invalid client id'); + } + + if (!client.secret) { + throw new Error('Import: Client has no secret'); + } + + if (client.type === ClientType.write) { + throw new Error('Import: Client is not allowed to import'); + } + + if (!(await verifyPassword(clientSecret, client.secret))) { + throw new Error('Import: Invalid client secret'); + } + + return client; +} + export function validateClerkJwt(token?: string) { if (!token) { return null; diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000..fb6aa4b1 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,41 @@ +{ + "name": "@openpanel/cli", + "version": "0.0.1-beta", + "module": "index.ts", + "bin": { + "openpanel": "dist/bin/cli.js" + }, + "scripts": { + "build": "rm -rf dist && tsup", + "lint": "eslint .", + "format": "prettier --check \"**/*.{mjs,ts,md,json}\"", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "arg": "^5.0.2", + "glob": "^10.4.3", + "inquirer": "^9.3.5", + "ramda": "^0.29.1", + "zod": "^3.22.4" + }, + "devDependencies": { + "@openpanel/db": "workspace:^", + "@openpanel/eslint-config": "workspace:*", + "@openpanel/prettier-config": "workspace:*", + "@openpanel/sdk": "workspace:*", + "@openpanel/tsconfig": "workspace:*", + "@types/node": "^20.14.10", + "@types/ramda": "^0.30.1", + "eslint": "^8.48.0", + "prettier": "^3.0.3", + "tsup": "^7.2.0", + "typescript": "^5.2.2" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@openpanel/eslint-config/base" + ] + }, + "prettier": "@openpanel/prettier-config" +} \ No newline at end of file diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 00000000..23d70f30 --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,26 @@ +import arg from 'arg'; + +import importer from './importer'; + +function cli() { + const args = arg( + { + '--help': Boolean, + }, + { + permissive: true, + } + ); + + console.log('cli args', args); + + const [command] = args._; + + switch (command) { + case 'import': { + return importer(); + } + } +} + +cli(); diff --git a/packages/cli/src/importer/index.ts b/packages/cli/src/importer/index.ts new file mode 100644 index 00000000..3fe6238a --- /dev/null +++ b/packages/cli/src/importer/index.ts @@ -0,0 +1,199 @@ +import fs from 'fs'; +import path from 'path'; +import arg from 'arg'; +import { groupBy } from 'ramda'; + +import type { PostEventPayload } from '@openpanel/sdk'; + +const BATCH_SIZE = 10000; // Define your batch size +const SLEEP_TIME = 100; // Define your sleep time between batches + +function progress(value: string) { + process.stdout.clearLine(0); + process.stdout.cursorTo(0); + process.stdout.write(value); +} + +function stripMixpanelProperties(obj: Record) { + const properties = ['time', 'distinct_id']; + const result: Record = {}; + for (const key in obj) { + if (key.match(/^(\$|mp_)/) || properties.includes(key)) { + continue; + } + result[key] = obj[key]; + } + return result; +} + +function safeParse(json: string) { + try { + return JSON.parse(json); + } catch (error) { + return null; + } +} + +function parseFileContent(fileContent: string): { + event: string; + properties: { + time: number; + distinct_id?: string | number; + [key: string]: unknown; + }; +}[] { + try { + return JSON.parse(fileContent); + } catch (error) { + const lines = fileContent.trim().split('\n'); + return lines + .map((line, index) => { + const json = safeParse(line); + if (!json) { + console.log('Warning: Failed to parse JSON'); + console.log('Index:', index); + console.log('Line:', line); + } + return json; + }) + .filter(Boolean); + } +} + +export default function importer() { + const args = arg( + { + '--file': String, + }, + { + permissive: true, + } + ); + + if (!args['--file']) { + throw new Error('Missing --file argument'); + } + + const cwd = process.cwd(); + + const filePath = path.resolve(cwd, args['--file']); + const fileContent = parseFileContent(fs.readFileSync(filePath, 'utf-8')); + + // const groups = groupBy((event) => event.properties.$device_id, fileContent); + // const groupEntries = Object.entries(groups); + + // const profiles = new Map(); + + // for (const [deviceId, items] of Object.entries(groups)) { + // items.forEach((item) => { + // if (item.properties.distinct_id) { + // if (!profiles.has(item.properties.distinct_id)) { + // profiles.set(item.properties.distinct_id, []); + // } + // profiles.get(item.properties.distinct_id)!.push(item); + // } else { + // item.properties.$device_id + // } + // }) + // profiles. + // } + // console.log('Total:', groupEntries.length); + // console.log('Undefined:', groups.undefined?.length ?? 0); + + // const uniqueKeys = new Set(); + // groups.undefined.forEach((event) => { + // if (event.properties.distinct_id) { + // console.log(event); + // } + // }); + + // 1: group by device id + // 2: add session start, session end and populate session_id + // 3: check if distinct_id exists on any event + // - If it does, get all events with that distinct_id and NO device_id and within session_start and session_end + // - add add the session_id to those events + // 4: send all events to the server + + const events: PostEventPayload[] = fileContent + .slice() + .reverse() + .map((event) => { + if (event.properties.mp_lib === 'web') { + console.log(event); + } + return { + profileId: event.properties.distinct_id + ? String(event.properties.distinct_id) + : undefined, + name: event.event, + timestamp: new Date(event.properties.time * 1000).toISOString(), + properties: { + __country: event.properties.country_code, + __region: event.properties.$region, + __city: event.properties.$city, + __os: event.properties.$os, + __browser: event.properties.$browser, + __browser_version: event.properties.$browser_version, + __referrer: event.properties.$referrer, + __device_id: event.properties.$device_id, + }, + }; + }); + + const totalPages = Math.ceil(events.length / BATCH_SIZE); + const estimatedTime = (totalPages / 8) * SLEEP_TIME + (totalPages / 8) * 80; + console.log(`Estimated time: ${estimatedTime / 1000} seconds`); + + async function batcher(page: number) { + const batch = events.slice(page * BATCH_SIZE, (page + 1) * BATCH_SIZE); + + if (batch.length === 0) { + return; + } + + const size = Buffer.byteLength(JSON.stringify(batch)); + console.log(batch.length, size / (1024 * 1024)); + + // await fetch('http://localhost:3333/import/events', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // 'openpanel-client-id': 'dd3db204-dcf6-49e2-9e82-de01cba7e585', + // 'openpanel-client-secret': 'sec_293b903816e327e10c9d', + // }, + // body: JSON.stringify(batch), + // }); + + await new Promise((resolve) => setTimeout(resolve, SLEEP_TIME)); + } + + async function runBatchesInParallel( + totalPages: number, + concurrentBatches: number + ) { + let currentPage = 0; + + while (currentPage < totalPages) { + const promises = []; + for ( + let i = 0; + i < concurrentBatches && currentPage < totalPages; + i++, currentPage++ + ) { + console.log(`Sending batch ${currentPage}... %)`); + promises.push(batcher(currentPage)); + } + await Promise.all(promises); + } + } + console.log(totalPages); + + // Trigger the batches + try { + runBatchesInParallel(totalPages, 8); // Run 8 batches in parallel + } catch (e) { + console.log('ERROR?!', e); + } + + return null; +} diff --git a/packages/cli/src/importer/load.ts b/packages/cli/src/importer/load.ts new file mode 100644 index 00000000..0437b968 --- /dev/null +++ b/packages/cli/src/importer/load.ts @@ -0,0 +1,255 @@ +import { randomUUID } from 'crypto'; +import fs from 'fs'; +import { glob } from 'glob'; + +import type { IClickhouseEvent } from '@openpanel/db'; + +const BATCH_SIZE = 8000; // Define your batch size +const SLEEP_TIME = 100; // Define your sleep time between batches + +function progress(value: string) { + process.stdout.clearLine(0); + process.stdout.cursorTo(0); + process.stdout.write(value); +} + +function stripMixpanelProperties(obj: Record) { + const properties = ['time', 'distinct_id']; + const result: Record = {}; + for (const key in obj) { + if (key.match(/^(\$|mp_)/) || properties.includes(key)) { + continue; + } + result[key] = obj[key]; + } + return result; +} + +function safeParse(json: string) { + try { + return JSON.parse(json); + } catch (error) { + return null; + } +} + +function parseFileContent(fileContent: string): { + event: string; + properties: { + time: number; + distinct_id?: string | number; + [key: string]: unknown; + }; +}[] { + try { + return JSON.parse(fileContent); + } catch (error) { + const lines = fileContent.trim().split('\n'); + return lines + .map((line, index) => { + const json = safeParse(line); + if (!json) { + console.log('Warning: Failed to parse JSON'); + console.log('Index:', index); + console.log('Line:', line); + } + return json; + }) + .filter(Boolean); + } +} + +export async function loadFilesBatcher() { + const files = await glob(['../../../../Downloads/mp-data/*.txt'], { + root: '/', + }); + + function chunks(array: string[], size: number) { + const results = []; + while (array.length) { + results.push(array.splice(0, size)); + } + return results; + } + + const times = []; + const chunksArray = chunks(files, 5); + let chunkIndex = 0; + for (const chunk of chunksArray) { + if (times.length > 0) { + // Print out how much time is approximately left + const average = times.reduce((a, b) => a + b) / times.length; + const remaining = (chunksArray.length - chunkIndex) * average; + console.log(`Estimated time left: ${remaining / 1000 / 60} minutes`); + } + console.log('Processing chunk:', chunkIndex); + chunkIndex++; + const d = Date.now(); + await loadFiles(chunk); + times.push(Date.now() - d); + } +} + +async function loadFiles(files: string[] = []) { + const data: any[] = []; + const filesToParse = files.slice(0, 10); + + await new Promise((resolve) => { + filesToParse.forEach((file) => { + const readStream = fs.createReadStream(file); + const content: any[] = []; + + readStream.on('data', (chunk) => { + // console.log(`Received ${chunk.length} bytes of data.`); + content.push(chunk.toString('utf-8')); + }); + + readStream.on('end', () => { + console.log('Finished reading file:', file); + data.push(parseFileContent(content.join(''))); + if (data.length === filesToParse.length) { + resolve(1); + } + }); + + readStream.on('error', (error) => { + console.error('Error reading file:', error); + }); + }); + }); + + const events: IClickhouseEvent[] = data.flat().map((event) => { + if (event.properties.mp_lib === 'web') { + return { + profile_id: event.properties.distinct_id + ? String(event.properties.distinct_id) + : '', + name: event.event, + created_at: new Date(event.properties.time * 1000).toISOString(), + properties: stripMixpanelProperties(event.properties) as Record< + string, + string + >, + country: event.properties.country_code, + region: event.properties.$region, + city: event.properties.$city, + os: event.properties.$os, + browser: event.properties.$browser, + browser_version: event.properties.$browser_version + ? String(event.properties.$browser_version) + : '', + referrer: event.properties.$initial_referrer, + referrer_type: event.properties.$search_engine ? 'search' : '', // FIX (IN API) + referrer_name: event.properties.$search_engine ?? '', // FIX (IN API) + device_id: event.properties.$device_id, + session_id: '', + project_id: '', // FIX (IN API) + path: event.properties.$current_url, // FIX + origin: '', // FIX (IN API) + os_version: '', // FIX + model: '', + longitude: null, + latitude: null, + id: randomUUID(), + duration: 0, + device: '', // FIX + brand: '', + }; + } else { + return { + profile_id: event.properties.distinct_id + ? String(event.properties.distinct_id) + : '', + name: event.event, + created_at: new Date(event.properties.time * 1000).toISOString(), + properties: stripMixpanelProperties(event.properties) as Record< + string, + string + >, + country: event.properties.country_code ?? '', + region: event.properties.$region ?? '', + city: event.properties.$city ?? '', + os: event.properties.$os ?? '', + browser: event.properties.$browser ?? '', + browser_version: event.properties.$browser_version + ? String(event.properties.$browser_version) + : '', + referrer: event.properties.$initial_referrer ?? '', + referrer_type: event.properties.$search_engine ? 'search' : '', // FIX (IN API) + referrer_name: event.properties.$search_engine ?? '', // FIX (IN API) + device_id: event.properties.$device_id ?? '', + session_id: '', + project_id: '', // FIX (IN API) + path: event.properties.$current_url ?? '', // FIX + origin: '', // FIX (IN API) + os_version: '', // FIX + model: '', + longitude: null, + latitude: null, + id: randomUUID(), + duration: 0, + device: '', // FIX + brand: '', + }; + } + }); + + const totalPages = Math.ceil(events.length / BATCH_SIZE); + const estimatedTime = (totalPages / 8) * SLEEP_TIME + (totalPages / 8) * 80; + console.log(`Estimated time: ${estimatedTime / 1000} seconds`); + + async function batcher(page: number) { + const batch = events.slice(page * BATCH_SIZE, (page + 1) * BATCH_SIZE); + + if (batch.length === 0) { + return; + } + + // const size = Buffer.byteLength(JSON.stringify(batch)); + // console.log(batch.length, size / (1024 * 1024)); + + await fetch('http://localhost:3333/import/events', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'openpanel-client-id': 'dd3db204-dcf6-49e2-9e82-de01cba7e585', + 'openpanel-client-secret': 'sec_293b903816e327e10c9d', + }, + body: JSON.stringify(batch), + }); + + await new Promise((resolve) => setTimeout(resolve, SLEEP_TIME)); + } + + async function runBatchesInParallel( + totalPages: number, + concurrentBatches: number + ) { + let currentPage = 0; + + while (currentPage < totalPages) { + const promises = []; + for ( + let i = 0; + i < concurrentBatches && currentPage < totalPages; + i++, currentPage++ + ) { + progress( + `Sending batch ${currentPage} (${Math.round((currentPage / totalPages) * 100)}... %)` + ); + promises.push(batcher(currentPage)); + } + await Promise.all(promises); + } + } + console.log(totalPages); + + // Trigger the batches + try { + await runBatchesInParallel(totalPages, 8); // Run 8 batches in parallel + } catch (e) { + console.log('ERROR?!', e); + } +} + +loadFilesBatcher(); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 00000000..fa4341f1 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@openpanel/tsconfig/base.json", + "compilerOptions": { + "incremental": false, + "outDir": "dist" + }, + "exclude": ["dist"] +} diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts new file mode 100644 index 00000000..7355fc66 --- /dev/null +++ b/packages/cli/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; + +import config from '@openpanel/tsconfig/tsup.config.json' assert { type: 'json' }; + +export default defineConfig({ + ...(config as any), + entry: ['src/cli.ts'], + format: ['cjs', 'esm'], +}); diff --git a/packages/sdks/web/cdn.ts b/packages/sdks/web/cdn.ts index b9ce999e..9e606004 100644 --- a/packages/sdks/web/cdn.ts +++ b/packages/sdks/web/cdn.ts @@ -24,9 +24,11 @@ declare global { window.op = (t, ...args) => { // @ts-expect-error - const fn = op[t].bind(op); + const fn = op[t] ? op[t].bind(op) : undefined; if (typeof fn === 'function') { fn(...args); + } else { + console.warn(`op.js: ${t} is not a function`); } }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a0c4cee..c99e4413 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,6 +112,9 @@ importers: url-metadata: specifier: ^4.1.0 version: 4.1.0 + uuid: + specifier: ^9.0.1 + version: 9.0.1 zod: specifier: ^3.22.4 version: 3.22.4 @@ -814,6 +817,58 @@ importers: specifier: ^5.2.2 version: 5.3.3 + packages/cli: + dependencies: + arg: + specifier: ^5.0.2 + version: 5.0.2 + glob: + specifier: ^10.4.3 + version: 10.4.5 + inquirer: + specifier: ^9.3.5 + version: 9.3.6 + ramda: + specifier: ^0.29.1 + version: 0.29.1 + zod: + specifier: ^3.22.4 + version: 3.22.4 + devDependencies: + '@openpanel/db': + specifier: workspace:^ + version: link:../db + '@openpanel/eslint-config': + specifier: workspace:* + version: link:../../tooling/eslint + '@openpanel/prettier-config': + specifier: workspace:* + version: link:../../tooling/prettier + '@openpanel/sdk': + specifier: workspace:* + version: link:../sdks/sdk + '@openpanel/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + '@types/node': + specifier: ^20.14.10 + version: 20.14.11 + '@types/ramda': + specifier: ^0.30.1 + version: 0.30.1 + eslint: + specifier: ^8.48.0 + version: 8.56.0 + prettier: + specifier: ^3.0.3 + version: 3.2.5 + tsup: + specifier: ^7.2.0 + version: 7.3.0(typescript@5.3.3) + typescript: + specifier: ^5.2.2 + version: 5.3.3 + packages/common: dependencies: '@openpanel/constants': @@ -4246,6 +4301,11 @@ packages: dev: false optional: true + /@inquirer/figures@1.0.4: + resolution: {integrity: sha512-R7Gsg6elpuqdn55fBH2y9oYzrU/yKrSmIsDX4ROT51vohrECFzTf2zw9BfUbOW8xjfmM2QbVoVYdTwhrtEKWSQ==} + engines: {node: '>=18'} + dev: false + /@ioredis/commands@1.2.0: resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} dev: false @@ -7562,6 +7622,12 @@ packages: dependencies: undici-types: 5.26.5 + /@types/node@20.14.11: + resolution: {integrity: sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==} + dependencies: + undici-types: 5.26.5 + dev: true + /@types/nprogress@0.2.3: resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} dev: false @@ -7579,6 +7645,12 @@ packages: types-ramda: 0.29.7 dev: true + /@types/ramda@0.30.1: + resolution: {integrity: sha512-aoyF/ADPL6N+/NXXfhPWF+Qj6w1Cql59m9wX0Gi15uyF+bpzXeLd63HPdiTDE2bmLXfNcVufsDPKmbfOrOzTBA==} + dependencies: + types-ramda: 0.30.1 + dev: true + /@types/range-parser@1.2.7: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true @@ -8829,6 +8901,10 @@ packages: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} dev: false + /chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + dev: false + /charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} dev: false @@ -8949,6 +9025,11 @@ packages: engines: {node: '>=6'} dev: false + /cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + dev: false + /client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} dev: false @@ -11047,6 +11128,15 @@ packages: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} dev: false + /external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + dev: false + /fast-content-type-parse@1.1.0: resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} dev: false @@ -11653,6 +11743,18 @@ packages: minimatch: 9.0.3 minipass: 7.0.4 path-scurry: 1.10.1 + dev: false + + /glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 /glob@6.0.4: resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==} @@ -11681,6 +11783,7 @@ packages: /glob@7.1.7: resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} + deprecated: Glob versions prior to v9 are no longer supported dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -11692,6 +11795,7 @@ packages: /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -12176,6 +12280,24 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /inquirer@9.3.6: + resolution: {integrity: sha512-riK/iQB2ctwkpWYgjjWIRv3MBLt2gzb2Sj0JNQNbyTXgyXsLWcDPJ5WS5ZDTCx7BRFnJsARtYh+58fjP5M2Y0Q==} + engines: {node: '>=18'} + dependencies: + '@inquirer/figures': 1.0.4 + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + external-editor: 3.1.0 + mute-stream: 1.0.0 + ora: 5.4.1 + run-async: 3.0.0 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + dev: false + /internal-ip@4.3.0: resolution: {integrity: sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==} engines: {node: '>=6'} @@ -12608,6 +12730,14 @@ packages: '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 + dev: false + + /jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 /jake@10.8.7: resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} @@ -14310,6 +14440,12 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: false @@ -14350,6 +14486,11 @@ packages: /minipass@7.0.4: resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} engines: {node: '>=16 || 14 >=14.17'} + dev: false + + /minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} @@ -14424,6 +14565,11 @@ packages: msgpackr-extract: 3.0.2 dev: false + /mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: false + /mv@2.1.1: resolution: {integrity: sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==} engines: {node: '>=0.8.0'} @@ -15137,6 +15283,9 @@ packages: engines: {node: '>=6'} dev: false + /package-json-from-dist@1.0.0: + resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -15254,6 +15403,14 @@ packages: dependencies: lru-cache: 10.2.0 minipass: 7.0.4 + dev: false + + /path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + dependencies: + lru-cache: 10.2.0 + minipass: 7.1.2 /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -16717,6 +16874,11 @@ packages: fsevents: 2.3.3 dev: true + /run-async@3.0.0: + resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + engines: {node: '>=0.12.0'} + dev: false + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -16726,6 +16888,12 @@ packages: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} dev: false + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.6.2 + dev: false + /sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -17392,7 +17560,7 @@ packages: dependencies: '@jridgewell/gen-mapping': 0.3.3 commander: 4.1.1 - glob: 10.3.10 + glob: 10.4.5 lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.6 @@ -17930,6 +18098,12 @@ packages: ts-toolbelt: 9.6.0 dev: true + /types-ramda@0.30.1: + resolution: {integrity: sha512-1HTsf5/QVRmLzcGfldPFvkVsAdi1db1BBKzi7iW3KBUlOICg/nKnFS+jGqDJS3YD8VsWbAh7JiHeBvbsw8RPxA==} + dependencies: + ts-toolbelt: 9.6.0 + dev: true + /typescript@5.3.3: resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} @@ -18695,6 +18869,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + /yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + dev: false + /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false