From 329c0885d4defde150b7707c9918d898a95340d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Mon, 17 Feb 2025 20:54:54 +0100 Subject: [PATCH] improve(self-hosting): remove goose, custom migration, docs, remove zookeeper --- ROADMAP.md | 15 - apps/api/Dockerfile | 4 - apps/public/content/docs/index.mdx | 69 +- .../content/docs/self-hosting/changelog.mdx | 23 + .../content/docs/self-hosting/meta.json | 6 +- .../self-hosting/migrating-from-clerk.mdx | 2 +- .../docs/self-hosting/self-hosting.mdx | 70 +- captain-definition-api | 4 - captain-definition-dashboard | 4 - captain-definition-docs | 4 - captain-definition-public | 4 - captain-definition-worker | 4 - docker-compose.yml | 16 +- packages/db/code-migrations/3-init-ch.ts | 372 ++++++ packages/db/code-migrations/migrate.ts | 4 +- packages/db/index.ts | 2 +- .../db/migrations/20240906185616_init.sql | 112 -- .../20240907202846_optimize_profiles.sql | 44 - .../20240917184138_fix_profile_merges.sql | 55 - .../20241007210706_retention_mv.sql | 58 - .../20241127133914_cluster_prep.sql | 351 ------ packages/db/migrations/goose | 28 - packages/db/package.json | 6 +- packages/db/src/buffers/bot-buffer-redis.ts | 2 +- packages/db/src/buffers/buffer.ts | 266 ---- packages/db/src/buffers/event-buffer-redis.ts | 2 +- .../db/src/buffers/profile-buffer-redis.ts | 2 +- .../client.ts} | 0 packages/db/src/clickhouse/migration.ts | 454 +++++++ packages/db/src/services/chart.service.ts | 2 +- packages/db/src/services/event.service.ts | 2 +- .../db/src/services/organization.service.ts | 4 + packages/db/src/services/profile.service.ts | 2 +- packages/db/src/services/retention.service.ts | 2 +- packages/db/src/sql-builder.ts | 2 +- packages/email/src/index.tsx | 13 +- packages/trpc/src/routers/auth.ts | 50 +- self-hosting/.env.template | 2 + self-hosting/clickhouse/clickhouse-config.xml | 33 +- .../clickhouse/clickhouse-keeper-config.xml | 44 - self-hosting/docker-compose.template.yml | 14 - self-hosting/package-lock.json | 1102 +++++++++++++++++ self-hosting/package.json | 2 +- self-hosting/pnpm-lock.yaml | 717 ----------- self-hosting/quiz.ts | 6 +- self-hosting/setup | 15 +- self-hosting/update | 11 + 47 files changed, 2181 insertions(+), 1825 deletions(-) delete mode 100644 ROADMAP.md create mode 100644 apps/public/content/docs/self-hosting/changelog.mdx delete mode 100644 captain-definition-api delete mode 100644 captain-definition-dashboard delete mode 100644 captain-definition-docs delete mode 100644 captain-definition-public delete mode 100644 captain-definition-worker create mode 100644 packages/db/code-migrations/3-init-ch.ts delete mode 100644 packages/db/migrations/20240906185616_init.sql delete mode 100644 packages/db/migrations/20240907202846_optimize_profiles.sql delete mode 100644 packages/db/migrations/20240917184138_fix_profile_merges.sql delete mode 100644 packages/db/migrations/20241007210706_retention_mv.sql delete mode 100644 packages/db/migrations/20241127133914_cluster_prep.sql delete mode 100755 packages/db/migrations/goose delete mode 100644 packages/db/src/buffers/buffer.ts rename packages/db/src/{clickhouse-client.ts => clickhouse/client.ts} (100%) create mode 100644 packages/db/src/clickhouse/migration.ts delete mode 100644 self-hosting/clickhouse/clickhouse-keeper-config.xml create mode 100644 self-hosting/package-lock.json delete mode 100644 self-hosting/pnpm-lock.yaml create mode 100755 self-hosting/update diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 4eb2bc6d..00000000 --- a/ROADMAP.md +++ /dev/null @@ -1,15 +0,0 @@ -# Roadmap - -## Simple todos - -- [ ] add session_id on events table, link this id on create -- [ ] add overview page containing - - [x] User histogram (last 30 minutes) - - [ ] Bounce rate - - [ ] Session duration - - [ ] Views per session - - [ ] Unique users - - [ ] Total users - - [ ] Total pageviews - - [ ] Total events -- [ ] diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 38db1194..7977135e 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -15,10 +15,6 @@ RUN corepack enable && apt-get update && \ && apt-get clean && \ rm -rf /var/lib/apt/lists/* -RUN curl -fsSL \ - https://raw.githubusercontent.com/pressly/goose/master/install.sh |\ - sh - ARG DATABASE_URL ENV DATABASE_URL=$DATABASE_URL ENV PNPM_HOME="/pnpm" diff --git a/apps/public/content/docs/index.mdx b/apps/public/content/docs/index.mdx index 60824d57..3751c719 100644 --- a/apps/public/content/docs/index.mdx +++ b/apps/public/content/docs/index.mdx @@ -1,14 +1,75 @@ --- title: Introduction to OpenPanel -description: The OpenPanel SDKs provide a set of core methods that allow you to track events, identify users, and more. Here's an overview of the key methods available in the SDKs. +description: Get started with OpenPanel's powerful analytics platform that combines the best of product and web analytics in one simple solution. --- - While all OpenPanel SDKs share a common set of core methods, some may have - syntax variations or additional methods specific to their environment. This - documentation provides an overview of the base methods and available SDKs. + OpenPanel is currently in beta and free to use. We're constantly improving our + platform based on user feedback. +## What is OpenPanel? + +OpenPanel is an open-source analytics platform that combines product analytics (like Mixpanel) with web analytics (like Plausible) into one simple solution. Whether you're tracking website visitors or analyzing user behavior in your app, OpenPanel provides the insights you need without the complexity. + +## Key Features + +### Web Analytics +- **Real-time data**: See visitor activity as it happens +- **Traffic sources**: Understand where your visitors come from +- **Geographic insights**: Track visitor locations and trends +- **Device analytics**: Monitor usage across different devices +- **Page performance**: Analyze your most visited pages + +### Product Analytics +- **Event tracking**: Monitor user actions and interactions +- **User profiles**: Build detailed user journey insights +- **Funnels**: Analyze conversion paths +- **Retention**: Track user engagement over time +- **Custom properties**: Add context to your events + +## Getting Started + +1. **Installation**: Choose your preferred method: + - [Script tag](/docs/sdks/script) - Quickest way to get started + - [Web SDK](/docs/sdks/web) - For more control and TypeScript support + - [React](/docs/sdks/react) - Native React integration + - [Next.js](/docs/sdks/nextjs) - Optimized for Next.js apps + +2. **Core Methods**: + ```js + // Track an event + track('button_clicked', { + buttonId: 'signup', + location: 'header' + }); + + // Identify a user + identify({ + profileId: 'user123', + email: 'user@example.com', + firstName: 'John' + }); + ``` + +## Privacy First + +OpenPanel is built with privacy in mind: +- No cookies required +- GDPR and CCPA compliant +- Self-hosting option available +- Full control over your data + +## Open Source + +OpenPanel is fully open-source and available on [GitHub](https://github.com/Openpanel-dev/openpanel). We believe in transparency and community-driven development. + +## Need Help? + +- Join our [Discord community](https://discord.gg/openpanel) +- Check our [GitHub issues](https://github.com/Openpanel-dev/openpanel/issues) +- Email us at [hello@openpanel.dev](mailto:hello@openpanel.dev) + ## Core Methods ### Set global properties diff --git a/apps/public/content/docs/self-hosting/changelog.mdx b/apps/public/content/docs/self-hosting/changelog.mdx new file mode 100644 index 00000000..6a0308f3 --- /dev/null +++ b/apps/public/content/docs/self-hosting/changelog.mdx @@ -0,0 +1,23 @@ +--- +title: Changelog for self-hosting +description: This is a list of changes that have been made to the self-hosting setup. +--- + +## 1.0.0 (stable) + +OpenPanel self-hosting is now in a stable state and should not be any breaking changes in the future. + +If you are upgrading from a previous version, you should keep an eye on the logs since it well tell you if you need to take any actions. Its not mandatory but its recommended since it might bite you in the *ss later. + +### New environment variables. + + +If you upgrading from a previous version, you'll need to edit your `.env` file if you want to use these new variables. + + +- `ALLOW_REGISTRATION` - If set to `false` new users will not be able to register (only the first user can register). +- `ALLOW_INVITATION` - If set to `false` new users will not be able to be invited. + +## 0.0.6 + +Removed Clerk.com and added self-hosted authentication. \ No newline at end of file diff --git a/apps/public/content/docs/self-hosting/meta.json b/apps/public/content/docs/self-hosting/meta.json index 0fcf0262..9c830d12 100644 --- a/apps/public/content/docs/self-hosting/meta.json +++ b/apps/public/content/docs/self-hosting/meta.json @@ -1,5 +1,9 @@ { "title": "Self-hosting", "defaultOpen": true, - "pages": ["self-hosting", "migrating-from-clerk"] + "pages": [ + "[Get started](/docs/self-hosting/self-hosting)", + "changelog", + "migrating-from-clerk" + ] } diff --git a/apps/public/content/docs/self-hosting/migrating-from-clerk.mdx b/apps/public/content/docs/self-hosting/migrating-from-clerk.mdx index ad7d48ba..113f1a95 100644 --- a/apps/public/content/docs/self-hosting/migrating-from-clerk.mdx +++ b/apps/public/content/docs/self-hosting/migrating-from-clerk.mdx @@ -47,7 +47,7 @@ docker compose cp ./users-dump.csv op-api:/app/packages/db/code-migrations/users Run the migration: ```bash -docker compose exec -it op-api bash -c "cd /app/packages/db && pnpm migrate:deploy:db:code 2-accounts.ts" +docker compose exec -it op-api bash -c "cd /app/packages/db && pnpm migrate:deploy:code 2-accounts.ts" ``` diff --git a/apps/public/content/docs/self-hosting/self-hosting.mdx b/apps/public/content/docs/self-hosting/self-hosting.mdx index 67277ccc..22be4858 100644 --- a/apps/public/content/docs/self-hosting/self-hosting.mdx +++ b/apps/public/content/docs/self-hosting/self-hosting.mdx @@ -1,13 +1,10 @@ --- -title: Self-hosting +title: Get started with self-hosting description: This is a simple guide how to get started with OpenPanel on your own VPS. --- import { Step, Steps } from 'fumadocs-ui/components/steps'; -OpenPanel is not stable yet. If you still want to self-host you can go ahead. Bear in mind that new changes might give a little headache to keep up with. -From version 0.0.5 we have removed Clerk.com. If you are upgrading from a previous version, you will need to export your users from Clerk and import them into OpenPanel. Read more about it here: [Migrating from Clerk](/docs/self-hosting/migrating-from-clerk) - ## Instructions ### Prerequisites @@ -54,8 +51,8 @@ cd openpanel/self-hosting 1. Install docker 2. Install node -3. Install pnpm -4. Run the `npx jiti ./quiz.ts` script inside the self-hosting folder +3. Install npm +4. Run the `npm run quiz` script inside the self-hosting folder @@ -110,6 +107,7 @@ Some of OpenPanel's features require e-mail. We use Resend as our transactional This is nothing that is required for the basic setup, but it is required for some features. Features that require e-mail: + - Password reset - Invitations - more will be added over time @@ -120,4 +118,62 @@ If you use a managed Redis service, you may need to set the `notify-keyspace-eve Without this setting we wont be able to listen for expired keys which we use for caluclating currently active vistors. -> You will see a warning in the logs if this needs to be set manually. \ No newline at end of file +> You will see a warning in the logs if this needs to be set manually. + +### Registration / Invitations + +By default registrations are disabled after the first user is created. + +You can change this by setting the `ALLOW_REGISTRATION` environment variable to `true`. + +```bash title=".env" +ALLOW_REGISTRATION=true +``` + +Invitations are enabled by default. You can also disable invitations by setting the `ALLOW_INVITATION` environment variable to `false`. + +```bash title=".env" +ALLOW_INVITATION=false +``` + +## Helpful scripts + +OpenPanel comes with several utility scripts to help manage your self-hosted instance: + +### Basic Operations + +```bash +./start # Start all OpenPanel services +./stop # Stop all OpenPanel services +./logs # View real-time logs from all services +``` + +### Maintenance + +```bash +./rebuild # Rebuild and restart a specific service + # Example: ./rebuild op-dashboard +``` + +### Troubleshooting + +```bash +./danger_wipe_everything # ⚠️ Removes all containers, volumes, and data + # Only use this if you want to start fresh! +``` + + +The `danger_wipe_everything` script will delete all your OpenPanel data including databases, configurations, and cached files. Use with extreme caution! + + +All these scripts should be run from within the `self-hosting` directory. Make sure the scripts are executable (`chmod +x script-name` if needed). + +## Updating + +To grab the latest and greatest from OpenPanel you should just run the `./update` script inside the self-hosting folder. + + + If you don't have the `./update` script, you can run `git pull` and then `./update` + + +Also read any changes in the [changelog](/changelog) and apply them to your instance. \ No newline at end of file diff --git a/captain-definition-api b/captain-definition-api deleted file mode 100644 index 53da2dd3..00000000 --- a/captain-definition-api +++ /dev/null @@ -1,4 +0,0 @@ -{ - "schemaVersion": 2, - "dockerfilePath": "./apps/api/Dockerfile" -} diff --git a/captain-definition-dashboard b/captain-definition-dashboard deleted file mode 100644 index 921af95f..00000000 --- a/captain-definition-dashboard +++ /dev/null @@ -1,4 +0,0 @@ -{ - "schemaVersion": 2, - "dockerfilePath": "./apps/dashboard/Dockerfile" -} diff --git a/captain-definition-docs b/captain-definition-docs deleted file mode 100644 index 56436874..00000000 --- a/captain-definition-docs +++ /dev/null @@ -1,4 +0,0 @@ -{ - "schemaVersion": 2, - "dockerfilePath": "./apps/docs/Dockerfile" -} diff --git a/captain-definition-public b/captain-definition-public deleted file mode 100644 index 92d4674c..00000000 --- a/captain-definition-public +++ /dev/null @@ -1,4 +0,0 @@ -{ - "schemaVersion": 2, - "dockerfilePath": "./apps/public/Dockerfile" -} diff --git a/captain-definition-worker b/captain-definition-worker deleted file mode 100644 index 640e0411..00000000 --- a/captain-definition-worker +++ /dev/null @@ -1,4 +0,0 @@ -{ - "schemaVersion": 2, - "dockerfilePath": "./apps/worker/Dockerfile" -} diff --git a/docker-compose.yml b/docker-compose.yml index 9f2db7cb..0ec281cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,7 @@ services: - ./docker/data/op-ch-logs:/var/log/clickhouse-server - ./self-hosting/clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/op-config.xml - ./self-hosting/clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/op-user-config.xml + - ./self-hosting/clickhouse/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro ulimits: nofile: soft: 262144 @@ -43,18 +44,3 @@ services: - "8123:8123" # HTTP interface - "9000:9000" # Native/TCP interface - "9009:9009" # Inter-server communication - - op-zk: - image: clickhouse/clickhouse-server:24.3.2-alpine - volumes: - - ./docker/data/op-zk-data:/var/lib/clickhouse - - ./self-hosting/clickhouse/clickhouse-keeper-config.xml:/etc/clickhouse-server/config.xml - command: [ 'clickhouse-keeper', '--config-file', '/etc/clickhouse-server/config.xml' ] - restart: always - ulimits: - nofile: - soft: 262144 - hard: 262144 - ports: - - "9181:9181" # Keeper port - - "9234:9234" # Keeper Raft port diff --git a/packages/db/code-migrations/3-init-ch.ts b/packages/db/code-migrations/3-init-ch.ts new file mode 100644 index 00000000..ca12a217 --- /dev/null +++ b/packages/db/code-migrations/3-init-ch.ts @@ -0,0 +1,372 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { formatClickhouseDate } from '../src/clickhouse/client'; +import { + createDatabase, + createMaterializedView, + createTable, + dropTable, + getExistingTables, + moveDataBetweenTables, + renameTable, + runClickhouseMigrationCommands, +} from '../src/clickhouse/migration'; +import { printBoxMessage } from './helpers'; + +export async function up() { + const replicatedVersion = '1'; + const existingTables = await getExistingTables(); + const hasSelfHosting = existingTables.includes('self_hosting_distributed'); + const hasEvents = existingTables.includes('events_distributed'); + const hasEventsV2 = existingTables.includes('events_v2'); + const hasEventsBots = existingTables.includes('events_bots_distributed'); + const hasProfiles = existingTables.includes('profiles_distributed'); + const hasProfileAliases = existingTables.includes( + 'profile_aliases_distributed', + ); + + const isSelfHosting = !!process.env.SELF_HOSTING; + const isClustered = !isSelfHosting; + + const isSelfHostingPostCluster = + existingTables.includes('events_replicated') && isSelfHosting; + + const isSelfHostingPreCluster = + !isSelfHostingPostCluster && + existingTables.includes('events_v2') && + isSelfHosting; + + const isSelfHostingOld = existingTables.length !== 0 && isSelfHosting; + + const sqls: string[] = []; + + // Move tables to old names if they exists + if (isSelfHostingOld) { + sqls.push( + ...existingTables + .filter((table) => { + return ( + !table.endsWith('_tmp') && !existingTables.includes(`${table}_tmp`) + ); + }) + .flatMap((table) => { + return renameTable({ + from: table, + to: `${table}_tmp`, + isClustered: false, + }); + }), + ); + } + + sqls.push( + createDatabase('openpanel', isClustered), + // Create new tables + ...createTable({ + name: 'self_hosting', + columns: ['`created_at` Date', '`domain` String', '`count` UInt64'], + orderBy: ['domain', 'created_at'], + partitionBy: 'toYYYYMM(created_at)', + distributionHash: 'cityHash64(domain)', + replicatedVersion, + isClustered, + }), + ...createTable({ + name: 'events', + columns: [ + '`id` UUID DEFAULT generateUUIDv4()', + '`name` LowCardinality(String)', + '`sdk_name` LowCardinality(String)', + '`sdk_version` LowCardinality(String)', + '`device_id` String CODEC(ZSTD(3))', + '`profile_id` String CODEC(ZSTD(3))', + '`project_id` String CODEC(ZSTD(3))', + '`session_id` String CODEC(LZ4)', + '`path` String CODEC(ZSTD(3))', + '`origin` String CODEC(ZSTD(3))', + '`referrer` String CODEC(ZSTD(3))', + '`referrer_name` String CODEC(ZSTD(3))', + '`referrer_type` LowCardinality(String)', + '`duration` UInt64 CODEC(Delta(4), LZ4)', + '`properties` Map(String, String) CODEC(ZSTD(3))', + '`created_at` DateTime64(3) CODEC(DoubleDelta, ZSTD(3))', + '`country` LowCardinality(FixedString(2))', + '`city` String', + '`region` LowCardinality(String)', + '`longitude` Nullable(Float32) CODEC(Gorilla, LZ4)', + '`latitude` Nullable(Float32) CODEC(Gorilla, LZ4)', + '`os` LowCardinality(String)', + '`os_version` LowCardinality(String)', + '`browser` LowCardinality(String)', + '`browser_version` LowCardinality(String)', + '`device` LowCardinality(String)', + '`brand` LowCardinality(String)', + '`model` LowCardinality(String)', + '`imported_at` Nullable(DateTime) CODEC(Delta(4), LZ4)', + ], + indices: [ + 'INDEX idx_name name TYPE bloom_filter GRANULARITY 1', + "INDEX idx_properties_bounce properties['__bounce'] TYPE set(3) GRANULARITY 1", + 'INDEX idx_origin origin TYPE bloom_filter(0.05) GRANULARITY 1', + 'INDEX idx_path path TYPE bloom_filter(0.01) GRANULARITY 1', + ], + orderBy: ['project_id', 'toDate(created_at)', 'profile_id', 'name'], + partitionBy: 'toYYYYMM(created_at)', + settings: { + index_granularity: 8192, + }, + distributionHash: + 'cityHash64(project_id, toString(toStartOfHour(created_at)))', + replicatedVersion, + isClustered, + }), + ...createTable({ + name: 'events_bots', + columns: [ + '`id` UUID DEFAULT generateUUIDv4()', + '`project_id` String', + '`name` String', + '`type` String', + '`path` String', + '`created_at` DateTime64(3)', + ], + orderBy: ['project_id', 'created_at'], + settings: { + index_granularity: 8192, + }, + distributionHash: + 'cityHash64(project_id, toString(toStartOfDay(created_at)))', + replicatedVersion, + isClustered, + }), + ...createTable({ + name: 'profiles', + columns: [ + '`id` String CODEC(ZSTD(3))', + '`is_external` Bool', + '`first_name` String CODEC(ZSTD(3))', + '`last_name` String CODEC(ZSTD(3))', + '`email` String CODEC(ZSTD(3))', + '`avatar` String CODEC(ZSTD(3))', + '`properties` Map(String, String) CODEC(ZSTD(3))', + '`project_id` String CODEC(ZSTD(3))', + '`created_at` DateTime64(3) CODEC(Delta(4), LZ4)', + ], + indices: [ + 'INDEX idx_first_name first_name TYPE bloom_filter GRANULARITY 1', + 'INDEX idx_last_name last_name TYPE bloom_filter GRANULARITY 1', + 'INDEX idx_email email TYPE bloom_filter GRANULARITY 1', + ], + engine: 'ReplacingMergeTree(created_at)', + orderBy: ['project_id', 'id'], + partitionBy: 'toYYYYMM(created_at)', + settings: { + index_granularity: 8192, + }, + distributionHash: 'cityHash64(project_id)', + replicatedVersion, + isClustered, + }), + ...createTable({ + name: 'profile_aliases', + columns: [ + '`project_id` String', + '`profile_id` String', + '`alias` String', + '`created_at` DateTime', + ], + orderBy: ['project_id', 'profile_id', 'alias', 'created_at'], + settings: { + index_granularity: 8192, + }, + distributionHash: 'cityHash64(project_id)', + replicatedVersion, + isClustered, + }), + + // Create materialized views + ...createMaterializedView({ + name: 'dau_mv', + tableName: 'events', + orderBy: ['project_id', 'date'], + partitionBy: 'toYYYYMMDD(date)', + query: `SELECT + toDate(created_at) as date, + uniqState(profile_id) as profile_id, + project_id + FROM {events} + GROUP BY date, project_id`, + distributionHash: 'cityHash64(project_id, date)', + replicatedVersion, + isClustered, + }), + ...createMaterializedView({ + name: 'cohort_events_mv', + tableName: 'events', + orderBy: ['project_id', 'name', 'created_at', 'profile_id'], + query: `SELECT + project_id, + name, + toDate(created_at) AS created_at, + profile_id, + COUNT() AS event_count + FROM {events} + WHERE profile_id != device_id + GROUP BY project_id, name, created_at, profile_id`, + distributionHash: 'cityHash64(project_id, toString(created_at))', + replicatedVersion, + isClustered, + }), + ...createMaterializedView({ + name: 'distinct_event_names_mv', + tableName: 'events', + orderBy: ['project_id', 'name', 'created_at'], + query: `SELECT + project_id, + name, + max(created_at) AS created_at, + count() AS event_count + FROM {events} + GROUP BY project_id, name`, + distributionHash: 'cityHash64(name, created_at)', + replicatedVersion, + isClustered, + }), + ...createMaterializedView({ + name: 'event_property_values_mv', + tableName: 'events', + orderBy: ['project_id', 'name', 'property_key', 'property_value'], + query: `SELECT + project_id, + name, + key_value.keys as property_key, + key_value.values as property_value, + created_at + FROM ( + SELECT + project_id, + name, + untuple(arrayJoin(properties)) as key_value, + max(created_at) as created_at + FROM {events} + GROUP BY project_id, name, key_value + ) + WHERE property_value != '' + AND property_key != '' + AND property_key NOT IN ('__duration_from', '__properties_from') + GROUP BY project_id, name, property_key, property_value, created_at`, + distributionHash: 'cityHash64(project_id, name)', + replicatedVersion, + isClustered, + }), + ); + + if (isSelfHostingPostCluster) { + sqls.push( + // Move data between tables + ...(hasSelfHosting + ? moveDataBetweenTables({ + from: 'self_hosting_replicated_tmp', + to: 'self_hosting', + batch: { + column: 'created_at', + interval: 'month', + transform: (date) => { + return formatClickhouseDate(date, true); + }, + }, + }) + : []), + ...(hasProfileAliases + ? moveDataBetweenTables({ + from: 'profile_aliases_replicated_tmp', + to: 'profile_aliases', + batch: { + column: 'created_at', + interval: 'month', + }, + }) + : []), + ...(hasEventsBots + ? moveDataBetweenTables({ + from: 'events_bots_replicated_tmp', + to: 'events_bots', + batch: { + column: 'created_at', + interval: 'month', + }, + }) + : []), + ...(hasProfiles + ? moveDataBetweenTables({ + from: 'profiles_replicated_tmp', + to: 'profiles', + batch: { + column: 'created_at', + interval: 'month', + }, + }) + : []), + ...(hasEvents + ? moveDataBetweenTables({ + from: 'events_replicated_tmp', + to: 'events', + batch: { + column: 'created_at', + interval: 'week', + }, + }) + : []), + ); + } + + if (isSelfHostingPreCluster) { + sqls.push( + ...(hasEventsV2 + ? moveDataBetweenTables({ + from: 'events_v2', + to: 'events', + batch: { + column: 'created_at', + interval: 'week', + }, + }) + : []), + ); + } + + fs.writeFileSync( + path.join(__dirname, '3-init-ch.sql'), + sqls + .map((sql) => + sql + .trim() + .replace(/;$/, '') + .replace(/\n{2,}/g, '\n') + .concat(';'), + ) + .join('\n\n---\n\n'), + ); + + printBoxMessage('Will start migration for self-hosting setup.', [ + 'This will move all data from the old tables to the new ones.', + 'This might take a while depending on your server.', + ]); + + if (!process.argv.includes('--dry')) { + await runClickhouseMigrationCommands(sqls); + } + + if (isSelfHostingOld) { + printBoxMessage( + '⚠️ Please run the following command to clean up unused tables:', + existingTables.map( + (table) => + `docker compose exec -it op-ch clickhouse-client --query "${dropTable( + `openpanel.${table}_tmp`, + false, + )}"`, + ), + ); + } +} diff --git a/packages/db/code-migrations/migrate.ts b/packages/db/code-migrations/migrate.ts index 621694d4..92b60fbe 100644 --- a/packages/db/code-migrations/migrate.ts +++ b/packages/db/code-migrations/migrate.ts @@ -5,7 +5,7 @@ import { printBoxMessage } from './helpers'; async function migrate() { const args = process.argv.slice(2); - const migration = args[0]; + const migration = args.filter((arg) => !arg.startsWith('--'))[0]; const migrationsDir = path.join(__dirname, '..', 'code-migrations'); const migrations = fs.readdirSync(migrationsDir).filter((file) => { @@ -22,7 +22,7 @@ async function migrate() { for (const file of migrations) { if (finishedMigrations.some((migration) => migration.name === file)) { - printBoxMessage('⏭️ Skipping Migration ⏭️', [`${file}`]); + printBoxMessage('✅ Already Migrated ✅', [`${file}`]); continue; } diff --git a/packages/db/index.ts b/packages/db/index.ts index 05990e53..4305d179 100644 --- a/packages/db/index.ts +++ b/packages/db/index.ts @@ -1,5 +1,5 @@ export * from './src/prisma-client'; -export * from './src/clickhouse-client'; +export * from './src/clickhouse/client'; export * from './src/sql-builder'; export * from './src/services/chart.service'; export * from './src/services/clients.service'; diff --git a/packages/db/migrations/20240906185616_init.sql b/packages/db/migrations/20240906185616_init.sql deleted file mode 100644 index 4efdc28a..00000000 --- a/packages/db/migrations/20240906185616_init.sql +++ /dev/null @@ -1,112 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS self_hosting -( - created_at Date, - domain String, - count UInt64 -) -ENGINE = MergeTree() -ORDER BY (domain, created_at) -PARTITION BY toYYYYMM(created_at); --- +goose StatementEnd - --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS events_v2 ( - `id` UUID DEFAULT generateUUIDv4(), - `name` String, - `sdk_name` String, - `sdk_version` String, - `device_id` String, - `profile_id` String, - `project_id` String, - `session_id` String, - `path` String, - `origin` String, - `referrer` String, - `referrer_name` String, - `referrer_type` String, - `duration` UInt64, - `properties` Map(String, String), - `created_at` DateTime64(3), - `country` String, - `city` String, - `region` String, - `longitude` Nullable(Float32), - `latitude` Nullable(Float32), - `os` String, - `os_version` String, - `browser` String, - `browser_version` String, - `device` String, - `brand` String, - `model` String, - `imported_at` Nullable(DateTime), - INDEX idx_name name TYPE bloom_filter GRANULARITY 1, - INDEX idx_properties_bounce properties ['__bounce'] TYPE set (3) GRANULARITY 1, - INDEX idx_origin origin TYPE bloom_filter(0.05) GRANULARITY 1, - INDEX idx_path path TYPE bloom_filter(0.01) GRANULARITY 1 -) ENGINE = MergeTree PARTITION BY toYYYYMM(created_at) -ORDER BY - (project_id, toDate(created_at), profile_id, name) SETTINGS index_granularity = 8192; --- +goose StatementEnd - --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS events_bots ( - `id` UUID DEFAULT generateUUIDv4(), - `project_id` String, - `name` String, - `type` String, - `path` String, - `created_at` DateTime64(3) -) ENGINE MergeTree -ORDER BY - (project_id, created_at) SETTINGS index_granularity = 8192; --- +goose StatementEnd - --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS profiles ( - `id` String, - `is_external` Bool, - `first_name` String, - `last_name` String, - `email` String, - `avatar` String, - `properties` Map(String, String), - `project_id` String, - `created_at` DateTime -) ENGINE = ReplacingMergeTree(created_at) -ORDER BY - (id) SETTINGS index_granularity = 8192; --- +goose StatementEnd - --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS profile_aliases ( - `project_id` String, - `profile_id` String, - `alias` String, - `created_at` DateTime -) ENGINE = MergeTree -ORDER BY - (project_id, profile_id, alias, created_at) SETTINGS index_granularity = 8192; --- +goose StatementEnd - --- +goose StatementBegin -CREATE MATERIALIZED VIEW IF NOT EXISTS dau_mv ENGINE = AggregatingMergeTree() PARTITION BY toYYYYMMDD(date) -ORDER BY - (project_id, date) POPULATE AS -SELECT - toDate(created_at) as date, - uniqState(profile_id) as profile_id, - project_id -FROM - events_v2 -GROUP BY - date, - project_id; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -SELECT 'down SQL query'; --- +goose StatementEnd diff --git a/packages/db/migrations/20240907202846_optimize_profiles.sql b/packages/db/migrations/20240907202846_optimize_profiles.sql deleted file mode 100644 index 74aa2c5a..00000000 --- a/packages/db/migrations/20240907202846_optimize_profiles.sql +++ /dev/null @@ -1,44 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE profiles_tmp -( - `id` String, - `is_external` Bool, - `first_name` String, - `last_name` String, - `email` String, - `avatar` String, - `properties` Map(String, String), - `project_id` String, - `created_at` DateTime, - INDEX idx_first_name first_name TYPE bloom_filter GRANULARITY 1, - INDEX idx_last_name last_name TYPE bloom_filter GRANULARITY 1, - INDEX idx_email email TYPE bloom_filter GRANULARITY 1 -) -ENGINE = ReplacingMergeTree(created_at) -PARTITION BY toYYYYMM(created_at) -ORDER BY (project_id, created_at, id) -SETTINGS index_granularity = 8192; --- +goose StatementEnd --- +goose StatementBegin -INSERT INTO profiles_tmp SELECT - id, - is_external, - first_name, - last_name, - email, - avatar, - properties, - project_id, - created_at -FROM profiles; --- +goose StatementEnd --- +goose StatementBegin -OPTIMIZE TABLE profiles_tmp FINAL; --- +goose StatementEnd --- +goose StatementBegin -RENAME TABLE profiles TO profiles_old, profiles_tmp TO profiles; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE profiles_old; --- +goose StatementEnd diff --git a/packages/db/migrations/20240917184138_fix_profile_merges.sql b/packages/db/migrations/20240917184138_fix_profile_merges.sql deleted file mode 100644 index ef419637..00000000 --- a/packages/db/migrations/20240917184138_fix_profile_merges.sql +++ /dev/null @@ -1,55 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE profiles_fixed -( - `id` String, - `is_external` Bool, - `first_name` String, - `last_name` String, - `email` String, - `avatar` String, - `properties` Map(String, String), - `project_id` String, - `created_at` DateTime, - INDEX idx_first_name first_name TYPE bloom_filter GRANULARITY 1, - INDEX idx_last_name last_name TYPE bloom_filter GRANULARITY 1, - INDEX idx_email email TYPE bloom_filter GRANULARITY 1 -) -ENGINE = ReplacingMergeTree(created_at) -PARTITION BY toYYYYMM(created_at) -ORDER BY (project_id, id) -SETTINGS index_granularity = 8192; --- +goose StatementEnd - --- +goose StatementBegin -INSERT INTO profiles_fixed SELECT - id, - is_external, - first_name, - last_name, - email, - avatar, - properties, - project_id, - created_at -FROM profiles; --- +goose StatementEnd - --- +goose StatementBegin -OPTIMIZE TABLE profiles_fixed FINAL; --- +goose StatementEnd - --- +goose StatementBegin -RENAME TABLE profiles TO profiles_old, profiles_fixed TO profiles; --- +goose StatementEnd - --- +goose StatementBegin -DROP TABLE profiles_old; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin --- This is a destructive migration, so the down migration is not provided. --- If needed, you should restore from a backup. -SELECT 'down migration not implemented'; --- +goose StatementEnd diff --git a/packages/db/migrations/20241007210706_retention_mv.sql b/packages/db/migrations/20241007210706_retention_mv.sql deleted file mode 100644 index 58cce210..00000000 --- a/packages/db/migrations/20241007210706_retention_mv.sql +++ /dev/null @@ -1,58 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE MATERIALIZED VIEW cohort_events_mv ENGINE = AggregatingMergeTree() -ORDER BY (project_id, name, created_at, profile_id) POPULATE AS -SELECT project_id, - name, - toDate(created_at) AS created_at, - profile_id, - COUNT() AS event_count -FROM events_v2 -WHERE profile_id != device_id -GROUP BY project_id, - name, - created_at, - profile_id; --- +goose StatementEnd --- +goose StatementBegin -CREATE MATERIALIZED VIEW distinct_event_names_mv ENGINE = AggregatingMergeTree() -ORDER BY (project_id, name, created_at) POPULATE AS -SELECT project_id, - name, - max(created_at) AS created_at, - count() AS event_count -FROM events_v2 -GROUP BY project_id, - name; --- +goose StatementEnd --- +goose StatementBegin -CREATE MATERIALIZED VIEW event_property_values_mv ENGINE = AggregatingMergeTree() -ORDER BY (project_id, name, property_key, property_value) POPULATE AS -select project_id, - name, - key_value.keys as property_key, - key_value.values as property_value, - created_at -from ( - SELECT project_id, - name, - untuple(arrayJoin(properties)) as key_value, - max(created_at) as created_at - from events_v2 - group by project_id, - name, - key_value - ) -where property_value != '' - and property_key != '' - and property_key NOT IN ('__duration_from', '__properties_from') -group by project_id, - name, - property_key, - property_value, - created_at; --- +goose StatementEnd --- +goose Down --- +goose StatementBegin -SELECT 'down SQL query'; --- +goose StatementEnd \ No newline at end of file diff --git a/packages/db/migrations/20241127133914_cluster_prep.sql b/packages/db/migrations/20241127133914_cluster_prep.sql deleted file mode 100644 index 4c779571..00000000 --- a/packages/db/migrations/20241127133914_cluster_prep.sql +++ /dev/null @@ -1,351 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE DATABASE IF NOT EXISTS openpanel; --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS self_hosting_replicated ON CLUSTER '{cluster}' ( - created_at Date, - domain String, - count UInt64 -) ENGINE = ReplicatedMergeTree( - '/clickhouse/tables/{shard}/self_hosting_replicated', - '{replica}' -) -ORDER BY (domain, created_at) PARTITION BY toYYYYMM(created_at); --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS events_replicated ON CLUSTER '{cluster}' ( - `id` UUID DEFAULT generateUUIDv4(), - `name` String, - `sdk_name` String, - `sdk_version` String, - `device_id` String, - `profile_id` String, - `project_id` String, - `session_id` String, - `path` String, - `origin` String, - `referrer` String, - `referrer_name` String, - `referrer_type` String, - `duration` UInt64, - `properties` Map(String, String), - `created_at` DateTime64(3), - `country` String, - `city` String, - `region` String, - `longitude` Nullable(Float32), - `latitude` Nullable(Float32), - `os` String, - `os_version` String, - `browser` String, - `browser_version` String, - `device` String, - `brand` String, - `model` String, - `imported_at` Nullable(DateTime), - INDEX idx_name name TYPE bloom_filter GRANULARITY 1, - INDEX idx_properties_bounce properties ['__bounce'] TYPE - set(3) GRANULARITY 1, - INDEX idx_origin origin TYPE bloom_filter(0.05) GRANULARITY 1, - INDEX idx_path path TYPE bloom_filter(0.01) GRANULARITY 1 -) ENGINE = ReplicatedMergeTree( - '/clickhouse/tables/{shard}/events_replicated', - '{replica}' -) PARTITION BY toYYYYMM(created_at) -ORDER BY (project_id, toDate(created_at), profile_id, name) SETTINGS index_granularity = 8192; --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS events_bots_replicated ON CLUSTER '{cluster}' ( - `id` UUID DEFAULT generateUUIDv4(), - `project_id` String, - `name` String, - `type` String, - `path` String, - `created_at` DateTime64(3) -) ENGINE = ReplicatedMergeTree( - '/clickhouse/tables/{shard}/events_bots_replicated', - '{replica}' -) -ORDER BY (project_id, created_at) SETTINGS index_granularity = 8192; --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS profiles_replicated ON CLUSTER '{cluster}' ( - `id` String, - `is_external` Bool, - `first_name` String, - `last_name` String, - `email` String, - `avatar` String, - `properties` Map(String, String), - `project_id` String, - `created_at` DateTime, - INDEX idx_first_name first_name TYPE bloom_filter GRANULARITY 1, - INDEX idx_last_name last_name TYPE bloom_filter GRANULARITY 1, - INDEX idx_email email TYPE bloom_filter GRANULARITY 1 -) ENGINE = ReplicatedReplacingMergeTree( - '/clickhouse/tables/{shard}/profiles_replicated', - '{replica}', - created_at -) PARTITION BY toYYYYMM(created_at) -ORDER BY (project_id, id) SETTINGS index_granularity = 8192; --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS profile_aliases_replicated ON CLUSTER '{cluster}' ( - `project_id` String, - `profile_id` String, - `alias` String, - `created_at` DateTime -) ENGINE = ReplicatedMergeTree( - '/clickhouse/tables/{shard}/profile_aliases_replicated', - '{replica}' -) -ORDER BY (project_id, profile_id, alias, created_at) SETTINGS index_granularity = 8192; --- +goose StatementEnd --- +goose StatementBegin -CREATE MATERIALIZED VIEW IF NOT EXISTS dau_mv_replicated ON CLUSTER '{cluster}' ENGINE = ReplicatedAggregatingMergeTree( - '/clickhouse/tables/{shard}/dau_mv_replicated', - '{replica}' -) PARTITION BY toYYYYMMDD(date) -ORDER BY (project_id, date) AS -SELECT toDate(created_at) as date, - uniqState(profile_id) as profile_id, - project_id -FROM events_replicated -GROUP BY date, - project_id; --- +goose StatementEnd --- +goose StatementBegin -CREATE MATERIALIZED VIEW IF NOT EXISTS cohort_events_mv_replicated ON CLUSTER '{cluster}' ENGINE = ReplicatedAggregatingMergeTree( - '/clickhouse/tables/{shard}/cohort_events_mv_replicated', - '{replica}' -) -ORDER BY (project_id, name, created_at, profile_id) AS -SELECT project_id, - name, - toDate(created_at) AS created_at, - profile_id, - COUNT() AS event_count -FROM events_replicated -WHERE profile_id != device_id -GROUP BY project_id, - name, - created_at, - profile_id; --- +goose StatementEnd --- +goose StatementBegin -CREATE MATERIALIZED VIEW IF NOT EXISTS distinct_event_names_mv_replicated ON CLUSTER '{cluster}' ENGINE = ReplicatedAggregatingMergeTree( - '/clickhouse/tables/{shard}/distinct_event_names_mv_replicated', - '{replica}' -) -ORDER BY (project_id, name, created_at) AS -SELECT project_id, - name, - max(created_at) AS created_at, - count() AS event_count -FROM events_replicated -GROUP BY project_id, - name; --- +goose StatementEnd --- +goose StatementBegin -CREATE MATERIALIZED VIEW IF NOT EXISTS event_property_values_mv_replicated ON CLUSTER '{cluster}' ENGINE = ReplicatedAggregatingMergeTree( - '/clickhouse/tables/{shard}/event_property_values_mv_replicated', - '{replica}' -) -ORDER BY (project_id, name, property_key, property_value) AS -SELECT project_id, - name, - key_value.keys as property_key, - key_value.values as property_value, - created_at -FROM ( - SELECT project_id, - name, - untuple(arrayJoin(properties)) as key_value, - max(created_at) as created_at - FROM events_replicated - GROUP BY project_id, - name, - key_value - ) -WHERE property_value != '' - AND property_key != '' - AND property_key NOT IN ('__duration_from', '__properties_from') -GROUP BY project_id, - name, - property_key, - property_value, - created_at; --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS self_hosting_distributed ON CLUSTER '{cluster}' AS self_hosting_replicated ENGINE = Distributed( - '{cluster}', - openpanel, - self_hosting_replicated, - cityHash64(domain) -); --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS events_distributed ON CLUSTER '{cluster}' AS events_replicated ENGINE = Distributed( - '{cluster}', - openpanel, - events_replicated, - cityHash64(project_id, toString(toStartOfHour(created_at))) -); --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS events_bots_distributed ON CLUSTER '{cluster}' AS events_bots_replicated ENGINE = Distributed( - '{cluster}', - openpanel, - events_bots_replicated, - cityHash64(project_id, toString(toStartOfDay(created_at))) -); --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS profiles_distributed ON CLUSTER '{cluster}' AS profiles_replicated ENGINE = Distributed( - '{cluster}', - openpanel, - profiles_replicated, - cityHash64(project_id) -); --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS dau_mv_distributed ON CLUSTER '{cluster}' AS dau_mv_replicated ENGINE = Distributed( - '{cluster}', - openpanel, - dau_mv_replicated, - rand() -); --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS cohort_events_mv_distributed ON CLUSTER '{cluster}' AS cohort_events_mv_replicated ENGINE = Distributed( - '{cluster}', - openpanel, - cohort_events_mv_replicated, - rand() -); --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS distinct_event_names_mv_distributed ON CLUSTER '{cluster}' AS distinct_event_names_mv_replicated ENGINE = Distributed( - '{cluster}', - openpanel, - distinct_event_names_mv_replicated, - rand() -); --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS event_property_values_mv_distributed ON CLUSTER '{cluster}' AS event_property_values_mv_replicated ENGINE = Distributed( - '{cluster}', - openpanel, - event_property_values_mv_replicated, - rand() -); --- +goose StatementEnd --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS profile_aliases_distributed ON CLUSTER '{cluster}' AS profile_aliases_replicated ENGINE = Distributed( - '{cluster}', - openpanel, - profile_aliases_replicated, - cityHash64(project_id) -); --- +goose StatementEnd --- +goose StatementBegin -INSERT INTO events_replicated -SELECT * -FROM events_v2; --- +goose StatementEnd --- +goose StatementBegin -INSERT INTO events_bots_replicated -SELECT * -FROM events_bots; --- +goose StatementEnd --- +goose StatementBegin -INSERT INTO profiles_replicated -SELECT * -FROM profiles; --- +goose StatementEnd --- +goose StatementBegin -INSERT INTO profile_aliases_replicated -SELECT * -FROM profile_aliases; --- +goose StatementEnd --- +goose StatementBegin -INSERT INTO self_hosting_replicated -SELECT * -FROM self_hosting; --- +goose StatementEnd --- +goose StatementBegin -INSERT INTO dau_mv_replicated -SELECT * -FROM dau_mv; --- +goose StatementEnd --- +goose StatementBegin -INSERT INTO cohort_events_mv_replicated -SELECT * -FROM cohort_events_mv; --- +goose StatementEnd --- +goose StatementBegin -INSERT INTO distinct_event_names_mv_replicated -SELECT * -FROM distinct_event_names_mv; --- +goose StatementEnd --- +goose StatementBegin -INSERT INTO event_property_values_mv_replicated -SELECT * -FROM event_property_values_mv; --- +goose StatementEnd --- +goose Down --- +goose StatementBegin -DROP TABLE IF EXISTS events_distributed ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS events_bots_distributed ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS profiles_distributed ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS events_replicated ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS events_bots_replicated ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS profiles_replicated ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS profile_aliases_replicated ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS dau_mv_replicated ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS cohort_events_mv_replicated ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS distinct_event_names_mv_replicated ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS event_property_values_mv_replicated ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS dau_mv_distributed ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS cohort_events_mv_distributed ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS distinct_event_names_mv_distributed ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd --- +goose StatementBegin -DROP TABLE IF EXISTS event_property_values_mv_distributed ON CLUSTER '{cluster}' SYNC; --- +goose StatementEnd -TRUNCATE TABLE events_replicated; -TRUNCATE TABLE events_bots_replicated; -TRUNCATE TABLE profiles_replicated; -TRUNCATE TABLE profile_aliases_replicated; -TRUNCATE TABLE self_hosting_replicated; -TRUNCATE TABLE dau_mv_replicated; -TRUNCATE TABLE cohort_events_mv_replicated; -TRUNCATE TABLE distinct_event_names_mv_replicated; -TRUNCATE TABLE event_property_values_mv_replicated; \ No newline at end of file diff --git a/packages/db/migrations/goose b/packages/db/migrations/goose deleted file mode 100755 index 9ae64324..00000000 --- a/packages/db/migrations/goose +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - - -if [ -n "$CLICKHOUSE_URL_DIRECT" ]; then - export GOOSE_DBSTRING=$CLICKHOUSE_URL_DIRECT -elif [ -z "$CLICKHOUSE_URL" ]; then - echo "Neither CLICKHOUSE_URL_DIRECT nor CLICKHOUSE_URL is set" - exit 1 -else - export GOOSE_DBSTRING=$CLICKHOUSE_URL -fi - -echo "Clickhouse migration script" -echo "" -echo "=================" -echo "Selected database: $GOOSE_DBSTRING" -echo "=================" -echo "" -if [ "$1" != "create" ] && [ -z "$CI" ]; then - read -p "Are you sure you want to run migrations on this database? (y/n) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Migration cancelled." - exit 0 - fi -fi - -goose clickhouse --dir ./migrations $@ \ No newline at end of file diff --git a/packages/db/package.json b/packages/db/package.json index 8b2f754f..6fafef47 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -3,13 +3,11 @@ "version": "0.0.1", "main": "index.ts", "scripts": { - "goose": "pnpm with-env ./migrations/goose", "codegen": "pnpm with-env prisma generate", "migrate": "pnpm with-env prisma migrate dev", - "migrate:deploy:db:code": "pnpm with-env jiti ./code-migrations/migrate.ts", + "migrate:deploy:code": "pnpm with-env jiti ./code-migrations/migrate.ts", "migrate:deploy:db": "pnpm with-env prisma migrate deploy", - "migrate:deploy:ch": "pnpm goose up", - "migrate:deploy": "pnpm migrate:deploy:db && pnpm migrate:deploy:db:code && pnpm migrate:deploy:ch", + "migrate:deploy": "pnpm migrate:deploy:db && pnpm migrate:deploy:code", "typecheck": "tsc --noEmit", "with-env": "dotenv -e ../../.env -c --" }, diff --git a/packages/db/src/buffers/bot-buffer-redis.ts b/packages/db/src/buffers/bot-buffer-redis.ts index fa8edd70..9a1b4a51 100644 --- a/packages/db/src/buffers/bot-buffer-redis.ts +++ b/packages/db/src/buffers/bot-buffer-redis.ts @@ -1,7 +1,7 @@ import { type Redis, getRedisCache, runEvery } from '@openpanel/redis'; import { getSafeJson } from '@openpanel/common'; -import { TABLE_NAMES, ch } from '../clickhouse-client'; +import { TABLE_NAMES, ch } from '../clickhouse/client'; import type { IClickhouseBotEvent } from '../services/event.service'; import { BaseBuffer } from './base-buffer'; diff --git a/packages/db/src/buffers/buffer.ts b/packages/db/src/buffers/buffer.ts deleted file mode 100644 index 1010c2d4..00000000 --- a/packages/db/src/buffers/buffer.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { generateId, getSafeJson } from '@openpanel/common'; -import type { ILogger } from '@openpanel/logger'; -import { createLogger } from '@openpanel/logger'; -import { getRedisCache } from '@openpanel/redis'; -import { pathOr } from 'ramda'; - -export type Find = ( - callback: (item: T) => boolean, -) => Promise; - -export type FindMany = ( - callback: (item: T) => boolean, -) => Promise; - -export class RedisBuffer { - public name: string; - protected prefix = 'op:buffer'; - protected bufferKey: string; - private lockKey: string; - protected maxBufferSize: number | null; - protected logger: ILogger; - - constructor(bufferName: string, maxBufferSize: number | null) { - this.name = bufferName; - this.bufferKey = bufferName; - this.lockKey = `lock:${bufferName}`; - this.maxBufferSize = maxBufferSize; - this.logger = createLogger({ name: 'buffer' }).child({ - buffer: bufferName, - }); - } - - protected getKey(name?: string) { - const key = `${this.prefix}:${this.bufferKey}`; - if (name) { - return `${key}:${name}`; - } - return key; - } - - async add(item: T): Promise { - try { - this.onAdd(item); - await getRedisCache().rpush(this.getKey(), JSON.stringify(item)); - const bufferSize = await getRedisCache().llen(this.getKey()); - - this.logger.debug( - `Item added (${pathOr('unknown', ['id'], item)}) Current size: ${bufferSize}`, - ); - - if (this.maxBufferSize && bufferSize >= this.maxBufferSize) { - await this.tryFlush(); - } - } catch (error) { - this.logger.error('Failed to add item to buffer', { error, item }); - } - } - - public async tryFlush(): Promise { - const lockId = generateId(); - const acquired = await getRedisCache().set( - this.lockKey, - lockId, - 'EX', - 60, - 'NX', - ); - - if (acquired === 'OK') { - this.logger.info(`Lock acquired. Attempting to flush. ID: ${lockId}`); - try { - await this.flush(); - } catch (error) { - this.logger.error(`Failed to flush buffer. ID: ${lockId}`, { error }); - } finally { - this.logger.info(`Releasing lock. ID: ${lockId}`); - await this.releaseLock(lockId); - } - } else { - this.logger.warn(`Failed to acquire lock. Skipping flush. ID: ${lockId}`); - } - } - - protected async waitForReleasedLock( - maxWaitTime = 8000, - checkInterval = 250, - ): Promise { - const startTime = performance.now(); - - while (performance.now() - startTime < maxWaitTime) { - const lock = await getRedisCache().get(this.lockKey); - if (!lock) { - return true; - } - - await new Promise((resolve) => setTimeout(resolve, checkInterval)); - } - - this.logger.warn('Timeout waiting for lock release'); - return false; - } - - private async retryOnce(cb: () => Promise) { - try { - await cb(); - } catch (e) { - this.logger.error(`#1 Failed to execute callback: ${cb.name}`, e); - await new Promise((resolve) => setTimeout(resolve, 1000)); - try { - await cb(); - } catch (e) { - this.logger.error(`#2 Failed to execute callback: ${cb.name}`, e); - throw e; - } - } - } - - private async flush(): Promise { - // Use a transaction to ensure atomicity - const result = await getRedisCache() - .multi() - .lrange(this.getKey(), 0, -1) - .lrange(this.getKey('backup'), 0, -1) - .del(this.getKey()) - .exec(); - - if (!result) { - this.logger.error('No result from redis transaction', { - result, - }); - throw new Error('Redis transaction failed'); - } - - const lrange = result[0]; - const lrangePrevious = result[1]; - - if (!lrange || lrange[0] instanceof Error) { - this.logger.error('Error from lrange', { - result, - }); - throw new Error('Redis transaction failed'); - } - - const items = lrange[1] as string[]; - if ( - lrangePrevious && - lrangePrevious[0] === null && - Array.isArray(lrangePrevious[1]) - ) { - items.push(...(lrangePrevious[1] as string[])); - } - - const parsedItems = items - .map((item) => getSafeJson(item) as T | null) - .filter((item): item is T => item !== null); - - if (parsedItems.length === 0) { - this.logger.debug('No items to flush'); - // Clear any existing backup since we have no items to process - await getRedisCache().del(this.getKey('backup')); - return; - } - - this.logger.info(`Flushing ${parsedItems.length} items`); - - try { - // Create backup before processing - await getRedisCache().del(this.getKey('backup')); // Clear any existing backup first - await getRedisCache().lpush( - this.getKey('backup'), - ...parsedItems.map((item) => JSON.stringify(item)), - ); - - const { toInsert, toKeep } = await this.processItems(parsedItems); - - if (toInsert.length) { - await this.retryOnce(() => this.insertIntoDB(toInsert)); - this.onInsert(toInsert); - } - - // Add back items to keep - if (toKeep.length > 0) { - await getRedisCache().lpush( - this.getKey(), - ...toKeep.map((item) => JSON.stringify(item)), - ); - } - - // Clear backup - await getRedisCache().del(this.getKey('backup')); - - this.logger.info( - `Inserted ${toInsert.length} items into DB, kept ${toKeep.length} items in buffer`, - { - toInsert: toInsert.length, - toKeep: toKeep.length, - }, - ); - } catch (error) { - this.logger.error('Failed to process queue while flushing buffer', { - error, - queueSize: parsedItems.length, - }); - - if (parsedItems.length > 0) { - // Add back items to keep - this.logger.info('Adding all items back to buffer'); - await getRedisCache().lpush( - this.getKey(), - ...parsedItems.map((item) => JSON.stringify(item)), - ); - } - - // Clear the backup since we're adding items back to main buffer - await getRedisCache().del(this.getKey('backup')); - } - } - - private async releaseLock(lockId: string): Promise { - this.logger.debug(`Released lock for ${this.getKey()}`); - const script = ` - if redis.call("get", KEYS[1]) == ARGV[1] then - return redis.call("del", KEYS[1]) - else - return 0 - end - `; - await getRedisCache().eval(script, 1, this.lockKey, lockId); - } - - protected async getQueue(count?: number): Promise { - try { - const items = await getRedisCache().lrange(this.getKey(), 0, count ?? -1); - return items - .map((item) => getSafeJson(item) as T | null) - .filter((item): item is T => item !== null); - } catch (error) { - this.logger.error('Failed to get queue', { error }); - return []; - } - } - - protected processItems(items: T[]): Promise<{ toInsert: T[]; toKeep: T[] }> { - return Promise.resolve({ toInsert: items, toKeep: [] }); - } - - protected insertIntoDB(_items: T[]): Promise { - throw new Error('Not implemented'); - } - - protected onAdd(_item: T): void { - // Override in subclass - } - - protected onInsert(_item: T[]): void { - // Override in subclass - } - - public findMany: FindMany = () => { - return Promise.resolve([]); - }; - - public find: Find = () => { - return Promise.resolve(null); - }; -} diff --git a/packages/db/src/buffers/event-buffer-redis.ts b/packages/db/src/buffers/event-buffer-redis.ts index 6fb72c44..da4db407 100644 --- a/packages/db/src/buffers/event-buffer-redis.ts +++ b/packages/db/src/buffers/event-buffer-redis.ts @@ -5,7 +5,7 @@ import { getRedisPub, runEvery, } from '@openpanel/redis'; -import { ch } from '../clickhouse-client'; +import { ch } from '../clickhouse/client'; import { type IClickhouseEvent, type IServiceEvent, diff --git a/packages/db/src/buffers/profile-buffer-redis.ts b/packages/db/src/buffers/profile-buffer-redis.ts index 337d78ea..97ca0c58 100644 --- a/packages/db/src/buffers/profile-buffer-redis.ts +++ b/packages/db/src/buffers/profile-buffer-redis.ts @@ -3,7 +3,7 @@ import { getSafeJson } from '@openpanel/common'; import { type Redis, getRedisCache } from '@openpanel/redis'; import { dissocPath, mergeDeepRight, omit, whereEq } from 'ramda'; -import { TABLE_NAMES, ch, chQuery } from '../clickhouse-client'; +import { TABLE_NAMES, ch, chQuery } from '../clickhouse/client'; import type { IClickhouseProfile } from '../services/profile.service'; import { BaseBuffer } from './base-buffer'; import { isPartialMatch } from './partial-json-match'; diff --git a/packages/db/src/clickhouse-client.ts b/packages/db/src/clickhouse/client.ts similarity index 100% rename from packages/db/src/clickhouse-client.ts rename to packages/db/src/clickhouse/client.ts diff --git a/packages/db/src/clickhouse/migration.ts b/packages/db/src/clickhouse/migration.ts new file mode 100644 index 00000000..53bffadd --- /dev/null +++ b/packages/db/src/clickhouse/migration.ts @@ -0,0 +1,454 @@ +import crypto from 'node:crypto'; +import { createClient } from './client'; +import { formatClickhouseDate } from './client'; + +interface CreateTableOptions { + name: string; + columns: string[]; + indices?: string[]; + engine?: string; + orderBy: string[]; + partitionBy?: string; + settings?: Record; + distributionHash: string; + replicatedVersion: string; + isClustered: boolean; +} + +interface CreateMaterializedViewOptions { + name: string; + tableName: string; + query: string; + engine?: string; + orderBy: string[]; + partitionBy?: string; + settings?: Record; + populate?: boolean; + distributionHash: string; + replicatedVersion: string; + isClustered: boolean; +} + +const CLUSTER_REPLICA_PATH = + '/clickhouse/{installation}/{cluster}/tables/{shard}/openpanel/v{replicatedVersion}/{table}'; + +const replicated = (tableName: string) => `${tableName}_replicated`; + +export const chMigrationClient = createClient({ + url: process.env.CLICKHOUSE_URL, + request_timeout: 3600000, // 1 hour in milliseconds + keep_alive: { + enabled: true, + idle_socket_ttl: 8000, + }, + compression: { + request: true, + }, + clickhouse_settings: { + wait_end_of_query: 1, + // Ask ClickHouse to periodically send query execution progress in HTTP headers, creating some activity in the connection. + send_progress_in_http_headers: 1, + // The interval of sending these progress headers. Here it is less than 60s, + http_headers_progress_interval_ms: '50000', + }, +}); + +export function createDatabase(name: string, isClustered: boolean) { + if (isClustered) { + return `CREATE DATABASE IF NOT EXISTS ${name} ON CLUSTER '{cluster}'`; + } + + return `CREATE DATABASE IF NOT EXISTS ${name}`; +} + +/** + * Creates SQL statements for table creation in ClickHouse + * Handles both clustered and non-clustered scenarios + */ +export function createTable({ + name: tableName, + columns, + indices = [], + engine = 'MergeTree()', + orderBy = ['tuple()'], + partitionBy, + settings = {}, + distributionHash, + replicatedVersion, + isClustered, +}: CreateTableOptions): string[] { + const columnDefinitions = [...columns, ...indices].join(',\n '); + + const settingsClause = Object.entries(settings).length + ? `SETTINGS ${Object.entries(settings) + .map(([key, value]) => `${key} = ${value}`) + .join(', ')}` + : ''; + + const partitionByClause = partitionBy ? `PARTITION BY ${partitionBy}` : ''; + + if (!isClustered) { + // Non-clustered scenario: single table + return [ + `CREATE TABLE IF NOT EXISTS ${tableName} ( + ${columnDefinitions} +) +ENGINE = ${engine} +${partitionByClause} +ORDER BY (${orderBy.join(', ')}) +${settingsClause}`.trim(), + ]; + } + + return [ + // Local replicated table + `CREATE TABLE IF NOT EXISTS ${replicated(tableName)} ON CLUSTER '{cluster}' ( + ${columnDefinitions} +) +ENGINE = Replicated${engine.replace(/^(.+?)\((.+?)?\)/, `$1('${CLUSTER_REPLICA_PATH.replace('{replicatedVersion}', replicatedVersion)}', '{replica}', $2)`).replace(/, \)$/, ')')} +${partitionByClause} +ORDER BY (${orderBy.join(', ')}) +${settingsClause}`.trim(), + // Distributed table + `CREATE TABLE IF NOT EXISTS ${tableName} ON CLUSTER '{cluster}' AS ${replicated(tableName)} +ENGINE = Distributed('{cluster}', currentDatabase(), ${replicated(tableName)}, ${distributionHash})`, + ]; +} + +/** + * Generates ALTER TABLE statements for adding columns + */ +export function addColumns( + tableName: string, + columns: string[], + isClustered: boolean, +): string[] { + if (isClustered) { + return columns.map( + (col) => + `ALTER TABLE ${replicated(tableName)} ON CLUSTER '{cluster}' ADD COLUMN IF NOT EXISTS ${col}`, + ); + } + + return columns.map( + (col) => `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS ${col}`, + ); +} + +/** + * Generates ALTER TABLE statements for dropping columns + */ +export function dropColumns( + tableName: string, + columnNames: string[], + isClustered: boolean, +): string[] { + if (isClustered) { + return columnNames.map( + (colName) => + `ALTER TABLE ${replicated(tableName)} ON CLUSTER '{cluster}' DROP COLUMN IF EXISTS ${colName}`, + ); + } + + return columnNames.map( + (colName) => `ALTER TABLE ${tableName} DROP COLUMN IF EXISTS ${colName}`, + ); +} + +export async function getExistingTables() { + try { + const existingTablesQuery = await chMigrationClient.query({ + query: `SELECT name FROM system.tables WHERE database = 'openpanel'`, + format: 'JSONEachRow', + }); + return (await existingTablesQuery.json<{ name: string }>()) + .map((table) => table.name) + .filter((table) => !table.includes('.inner_id')); + } catch (e) { + console.error(e); + return []; + } +} + +export function renameTable({ + from, + to, + isClustered, +}: { + from: string; + to: string; + isClustered: boolean; +}) { + if (isClustered) { + return [ + `RENAME TABLE ${replicated(from)} TO ${replicated(to)} ON CLUSTER '{cluster}'`, + `RENAME TABLE ${from} TO ${to} ON CLUSTER '{cluster}'`, + ]; + } + + return [`RENAME TABLE ${from} TO ${to}`]; +} + +export function dropTable(tableName: string, isClustered: boolean) { + if (isClustered) { + return `DROP TABLE IF EXISTS ${tableName} ON CLUSTER '{cluster}'`; + } + + return `DROP TABLE IF EXISTS ${tableName}`; +} + +export function moveDataBetweenTables({ + from, + to, + batch, +}: { + from: string; + to: string; + batch?: { + column: string; + interval?: 'day' | 'week' | 'month'; + transform?: (date: Date) => string; + endDate?: Date; + startDate?: Date; + }; +}): string[] { + const sqls: string[] = []; + + if (!batch) { + return [`INSERT INTO ${to} SELECT * FROM ${from}`]; + } + + // Start from today and go back 3 years + const endDate = batch.endDate || new Date(); + if (!batch.endDate) { + endDate.setDate(endDate.getDate() + 1); // Add 1 day to include today + } + const startDate = batch.startDate || new Date(); + if (!batch.startDate) { + startDate.setFullYear(startDate.getFullYear() - 3); + } + + let currentDate = endDate; + const interval = batch.interval || 'day'; + + while (currentDate > startDate) { + const previousDate = new Date(currentDate); + + switch (interval) { + case 'month': + previousDate.setMonth(previousDate.getMonth() - 1); + break; + case 'week': + previousDate.setDate(previousDate.getDate() - 7); + // Ensure we don't go below startDate + if (previousDate < startDate) { + previousDate.setTime(startDate.getTime()); + } + break; + // day + default: + previousDate.setDate(previousDate.getDate() - 1); + break; + } + + const sql = `INSERT INTO ${to} + SELECT * FROM ${from} + WHERE ${batch.column} > '${batch.transform ? batch.transform(previousDate) : formatClickhouseDate(previousDate, true)}' + AND ${batch.column} <= '${batch.transform ? batch.transform(currentDate) : formatClickhouseDate(currentDate, true)}'`; + sqls.push(sql); + + currentDate = previousDate; + } + + return sqls; +} + +export function createMaterializedView({ + name: tableName, + query, + engine = 'AggregatingMergeTree()', + orderBy, + partitionBy, + settings = {}, + populate = false, + distributionHash = 'rand()', + replicatedVersion, + isClustered, +}: CreateMaterializedViewOptions): string[] { + const settingsClause = Object.entries(settings).length + ? `SETTINGS ${Object.entries(settings) + .map(([key, value]) => `${key} = ${value}`) + .join(', ')}` + : ''; + + const partitionByClause = partitionBy ? `PARTITION BY ${partitionBy}` : ''; + + // Transform query to use replicated table names in clustered mode + const transformedQuery = query.replace(/\{(\w+)\}/g, (_, tableName) => + isClustered ? replicated(tableName) : tableName, + ); + + if (!isClustered) { + return [ + `CREATE MATERIALIZED VIEW IF NOT EXISTS ${tableName} +ENGINE = ${engine} +${partitionByClause} +ORDER BY (${orderBy.join(', ')}) +${settingsClause} +${populate ? 'POPULATE' : ''} +AS ${transformedQuery}`.trim(), + ]; + } + + return [ + // Replicated materialized view + `CREATE MATERIALIZED VIEW IF NOT EXISTS ${replicated(tableName)} ON CLUSTER '{cluster}' +ENGINE = Replicated${engine.replace(/^(.+?)\((.+?)?\)/, `$1('${CLUSTER_REPLICA_PATH.replace('{replicatedVersion}', replicatedVersion)}', '{replica}', $2)`).replace(/, \)$/, ')')} +${partitionByClause} +ORDER BY (${orderBy.join(', ')}) +${settingsClause} +${populate ? 'POPULATE' : ''} +AS ${transformedQuery}`.trim(), + // Distributed materialized view + `CREATE TABLE IF NOT EXISTS ${tableName} ON CLUSTER '{cluster}' AS ${replicated(tableName)} +ENGINE = Distributed('{cluster}', currentDatabase(), ${replicated(tableName)}, ${distributionHash})`, + ]; +} + +export function countRows(tableName: string) { + return `SELECT count() FROM ${tableName}`; +} + +export async function runClickhouseMigrationCommands(sqls: string[]) { + let abort: AbortController | undefined; + let activeQueryId: string | undefined; + + const handleTermination = async (signal: string) => { + console.warn( + `Received ${signal}. Cleaning up active queries before exit...`, + ); + + if (abort) { + abort.abort(); + } + }; + + // Create bound handler functions + const handleSigterm = () => handleTermination('SIGTERM'); + const handleSigint = () => handleTermination('SIGINT'); + + // Register handlers + process.on('SIGTERM', handleSigterm); + process.on('SIGINT', handleSigint); + + try { + for (const sql of sqls) { + abort = new AbortController(); + let timer: NodeJS.Timeout | undefined; + let resolve: ((value: unknown) => void) | undefined; + activeQueryId = crypto.createHash('sha256').update(sql).digest('hex'); + + console.log('----------------------------------------'); + console.log('---| Running query | Query ID:', activeQueryId); + console.log('---| SQL |------------------------------'); + console.log(sql); + console.log('----------------------------------------'); + + try { + const res = await Promise.race([ + chMigrationClient.command({ + query: sql, + query_id: activeQueryId, + abort_signal: abort?.signal, + }), + new Promise((r) => { + resolve = r; + let checking = false; // Add flag to prevent multiple concurrent checks + + async function check() { + if (checking) return; // Skip if already checking + checking = true; + + try { + const res = await chMigrationClient + .query({ + query: `SELECT + query_id, + elapsed, + read_rows, + written_rows, + memory_usage + FROM system.processes + WHERE query_id = '${activeQueryId}'`, + format: 'JSONEachRow', + }) + .then((res) => res.json()); + + const formatMemory = (bytes: number) => { + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let unitIndex = 0; + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + return `${Math.round(size * 100) / 100}${units[unitIndex]}`; + }; + + const formatNumber = (num: number) => { + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + }; + + if (Array.isArray(res) && res.length > 0) { + const { elapsed, read_rows, written_rows, memory_usage } = + res[0] as any; + console.log( + `Progress: ${elapsed.toFixed(2)}s | Memory: ${formatMemory(memory_usage)} | Read: ${formatNumber(read_rows)} rows | Written: ${formatNumber(written_rows)} rows`, + ); + } + } finally { + checking = false; + } + + timer = setTimeout(check, 5000); // Schedule next check after current one completes + } + + // Start the first check after 5 seconds + timer = setTimeout(check, 5000); + }), + ]); + + if (timer) { + clearTimeout(timer); + } + if (resolve) { + resolve(res); + } + } catch (e) { + console.log('Failed on query', sql); + throw e; + } + } + } catch (e) { + if (abort) { + abort.abort(); + } + + if (activeQueryId) { + try { + await chMigrationClient.command({ + query: `KILL QUERY WHERE query_id = '${activeQueryId}'`, + }); + console.log(`Successfully killed query ${activeQueryId}`); + } catch (err) { + console.error(`Failed to kill query ${activeQueryId}:`, err); + } + } + + throw e; + } finally { + // Clean up event listeners + process.off('SIGTERM', handleSigterm); + process.off('SIGINT', handleSigint); + } +} diff --git a/packages/db/src/services/chart.service.ts b/packages/db/src/services/chart.service.ts index 24e364b4..8ed5031c 100644 --- a/packages/db/src/services/chart.service.ts +++ b/packages/db/src/services/chart.service.ts @@ -13,7 +13,7 @@ import { TABLE_NAMES, formatClickhouseDate, toDate, -} from '../clickhouse-client'; +} from '../clickhouse/client'; import { createSqlBuilder } from '../sql-builder'; export function transformPropertyKey(property: string) { diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts index 3af54f4d..abacd5b2 100644 --- a/packages/db/src/services/event.service.ts +++ b/packages/db/src/services/event.service.ts @@ -12,7 +12,7 @@ import { chQuery, convertClickhouseDateToJs, formatClickhouseDate, -} from '../clickhouse-client'; +} from '../clickhouse/client'; import type { EventMeta, Prisma } from '../prisma-client'; import { db } from '../prisma-client'; import { createSqlBuilder } from '../sql-builder'; diff --git a/packages/db/src/services/organization.service.ts b/packages/db/src/services/organization.service.ts index 0d8a905b..9e7bc9ea 100644 --- a/packages/db/src/services/organization.service.ts +++ b/packages/db/src/services/organization.service.ts @@ -143,6 +143,10 @@ export async function connectUserToOrganization({ throw new Error('Invite not found'); } + if (process.env.ALLOW_INVITATION === 'false') { + throw new Error('Invitations are not allowed'); + } + if (invite.expiresAt < new Date()) { throw new Error('Invite expired'); } diff --git a/packages/db/src/services/profile.service.ts b/packages/db/src/services/profile.service.ts index 8e548fa2..fbfb4012 100644 --- a/packages/db/src/services/profile.service.ts +++ b/packages/db/src/services/profile.service.ts @@ -11,7 +11,7 @@ import { ch, chQuery, formatClickhouseDate, -} from '../clickhouse-client'; +} from '../clickhouse/client'; import { createSqlBuilder } from '../sql-builder'; export type IProfileMetrics = { diff --git a/packages/db/src/services/retention.service.ts b/packages/db/src/services/retention.service.ts index 64e6b24d..b7ce62b9 100644 --- a/packages/db/src/services/retention.service.ts +++ b/packages/db/src/services/retention.service.ts @@ -1,6 +1,6 @@ import { escape } from 'sqlstring'; -import { TABLE_NAMES, chQuery } from '../clickhouse-client'; +import { TABLE_NAMES, chQuery } from '../clickhouse/client'; type IGetWeekRetentionInput = { projectId: string; diff --git a/packages/db/src/sql-builder.ts b/packages/db/src/sql-builder.ts index 8d82fbdf..f022dee0 100644 --- a/packages/db/src/sql-builder.ts +++ b/packages/db/src/sql-builder.ts @@ -1,4 +1,4 @@ -import { TABLE_NAMES } from './clickhouse-client'; +import { TABLE_NAMES } from './clickhouse/client'; export interface SqlBuilderObject { where: Record; diff --git a/packages/email/src/index.tsx b/packages/email/src/index.tsx index 4e7730cb..cdfb5586 100644 --- a/packages/email/src/index.tsx +++ b/packages/email/src/index.tsx @@ -13,11 +13,6 @@ export async function sendEmail( data: z.infer; }, ) { - if (!process.env.RESEND_API_KEY) { - return null; - } - - const resend = new Resend(process.env.RESEND_API_KEY); const { to, data } = options; const { subject, Component, schema } = templates[template]; const props = schema.safeParse(data); @@ -27,6 +22,14 @@ export async function sendEmail( return null; } + if (!process.env.RESEND_API_KEY) { + console.log('No RESEND_API_KEY found, here is the data'); + console.log(data); + return null; + } + + const resend = new Resend(process.env.RESEND_API_KEY); + try { const res = await resend.emails.send({ from: FROM, diff --git a/packages/trpc/src/routers/auth.ts b/packages/trpc/src/routers/auth.ts index bb855b70..3aecfc00 100644 --- a/packages/trpc/src/routers/auth.ts +++ b/packages/trpc/src/routers/auth.ts @@ -37,6 +37,38 @@ import { const zProvider = z.enum(['email', 'google', 'github']); +async function getIsRegistrationAllowed(inviteId?: string | null) { + // ALLOW_REGISTRATION is always undefined in cloud + if (process.env.ALLOW_REGISTRATION === undefined) { + return true; + } + + // Self-hosting logic + // 1. First user is always allowed + const count = await db.user.count(); + if (count === 0) { + return true; + } + + // 2. If there is an invite, check if it is valid + if (inviteId) { + if (process.env.ALLOW_INVITATION === 'false') { + return false; + } + + const invite = await db.invite.findUnique({ + where: { + id: inviteId, + }, + }); + + return !!invite; + } + + // 3. Otherwise, check if general registration is allowed + return process.env.ALLOW_REGISTRATION !== 'false'; +} + export const authRouter = createTRPCRouter({ signOut: publicProcedure.mutation(async ({ ctx }) => { deleteSessionTokenCookie(ctx.setCookie); @@ -46,7 +78,15 @@ export const authRouter = createTRPCRouter({ }), signInOAuth: publicProcedure .input(z.object({ provider: zProvider, inviteId: z.string().nullish() })) - .mutation(({ input, ctx }) => { + .mutation(async ({ input, ctx }) => { + const isRegistrationAllowed = await getIsRegistrationAllowed( + input.inviteId, + ); + + if (!isRegistrationAllowed) { + throw TRPCAccessError('Registrations are not allowed'); + } + const { provider } = input; if (input.inviteId) { @@ -95,6 +135,14 @@ export const authRouter = createTRPCRouter({ signUpEmail: publicProcedure .input(zSignUpEmail) .mutation(async ({ input, ctx }) => { + const isRegistrationAllowed = await getIsRegistrationAllowed( + input.inviteId, + ); + + if (!isRegistrationAllowed) { + throw TRPCAccessError('Registrations are not allowed'); + } + const provider = 'email'; const user = await getUserAccount({ email: input.email, diff --git a/self-hosting/.env.template b/self-hosting/.env.template index ad6f0cd3..d7e13bd5 100644 --- a/self-hosting/.env.template +++ b/self-hosting/.env.template @@ -3,6 +3,8 @@ SELF_HOSTED="true" GEO_IP_HOST="http://op-geo:8080" BATCH_SIZE="5000" BATCH_INTERVAL="10000" +ALLOW_REGISTRATION="false" +ALLOW_INVITATION="true" # Will be replaced with the setup script REDIS_URL="$REDIS_URL" CLICKHOUSE_URL="$CLICKHOUSE_URL" diff --git a/self-hosting/clickhouse/clickhouse-config.xml b/self-hosting/clickhouse/clickhouse-config.xml index 0350eefc..d4bf45e9 100644 --- a/self-hosting/clickhouse/clickhouse-config.xml +++ b/self-hosting/clickhouse/clickhouse-config.xml @@ -5,13 +5,7 @@ 10 - - + @@ -25,29 +19,4 @@ 0.0.0.0 0.0.0.0 op-ch - - - 1 - replica1 - openpanel_cluster - - - - - op-zk - 9181 - - - - - - - true - - op-ch - 9000 - - - - \ No newline at end of file diff --git a/self-hosting/clickhouse/clickhouse-keeper-config.xml b/self-hosting/clickhouse/clickhouse-keeper-config.xml deleted file mode 100644 index 2ed39f0e..00000000 --- a/self-hosting/clickhouse/clickhouse-keeper-config.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - information - true - - - /var/lib/clickhouse/ - /var/lib/clickhouse/tmp/ - - /var/lib/clickhouse/user_files/ - - UTC - false - - 0.0.0.0 - 0.0.0.0 - op-zk - - - 9181 - :: - :: - 1 - /var/lib/clickhouse/coordination/log - /var/lib/clickhouse/coordination/snapshots - - - 10000 - 30000 - - - - - 1 - op-zk - 9234 - - - - - - /clickhouse/production/task_queue/ddl - - \ No newline at end of file diff --git a/self-hosting/docker-compose.template.yml b/self-hosting/docker-compose.template.yml index 5a2f8d19..2d07d33a 100644 --- a/self-hosting/docker-compose.template.yml +++ b/self-hosting/docker-compose.template.yml @@ -65,18 +65,6 @@ services: soft: 262144 hard: 262144 - op-zk: - image: clickhouse/clickhouse-server:24.3.2-alpine - volumes: - - op-zk-data:/var/lib/clickhouse - - ./clickhouse/clickhouse-keeper-config.xml:/etc/clickhouse-server/config.xml - command: [ 'clickhouse-keeper', '--config-file', '/etc/clickhouse-server/config.xml' ] - restart: always - ulimits: - nofile: - soft: 262144 - hard: 262144 - op-api: image: lindesvard/openpanel-api:latest restart: always @@ -139,5 +127,3 @@ volumes: driver: local op-proxy-config: driver: local - op-zk-data: - driver: local diff --git a/self-hosting/package-lock.json b/self-hosting/package-lock.json new file mode 100644 index 00000000..dcb3038b --- /dev/null +++ b/self-hosting/package-lock.json @@ -0,0 +1,1102 @@ +{ + "name": "@openpanel/self-hosting", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openpanel/self-hosting", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/inquirer": "^9.0.7", + "@types/js-yaml": "^4.0.9", + "bcrypt": "^5.1.1", + "inquirer": "^9.3.1", + "jiti": "^1.21.6", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.10.tgz", + "integrity": "sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/inquirer": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz", + "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==", + "dependencies": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" + }, + "node_modules/@types/node": { + "version": "22.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", + "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/inquirer": { + "version": "9.3.7", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.7.tgz", + "integrity": "sha512-LJKFHCSeIRq9hanN14IlOtPSTe3lNES7TYDTE2xxdAy1LS5rYphajK1qtwvj3YmQXvvk0U2Vbmcni8P9EIQW9w==", + "dependencies": { + "@inquirer/figures": "^1.0.3", + "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" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/self-hosting/package.json b/self-hosting/package.json index 821d0635..ce369b03 100644 --- a/self-hosting/package.json +++ b/self-hosting/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "quiz": "jiti quiz.ts" }, "keywords": [], "author": "", diff --git a/self-hosting/pnpm-lock.yaml b/self-hosting/pnpm-lock.yaml deleted file mode 100644 index 298fde24..00000000 --- a/self-hosting/pnpm-lock.yaml +++ /dev/null @@ -1,717 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -dependencies: - '@types/inquirer': - specifier: ^9.0.7 - version: 9.0.7 - '@types/js-yaml': - specifier: ^4.0.9 - version: 4.0.9 - bcrypt: - specifier: ^5.1.1 - version: 5.1.1 - inquirer: - specifier: ^9.3.1 - version: 9.3.6 - jiti: - specifier: ^1.21.6 - version: 1.21.6 - js-yaml: - specifier: ^4.1.0 - version: 4.1.0 - -devDependencies: - '@types/bcrypt': - specifier: ^5.0.2 - version: 5.0.2 - -packages: - - /@inquirer/figures@1.0.6: - resolution: {integrity: sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==} - engines: {node: '>=18'} - dev: false - - /@mapbox/node-pre-gyp@1.0.11: - resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} - hasBin: true - dependencies: - detect-libc: 2.0.3 - https-proxy-agent: 5.0.1 - make-dir: 3.1.0 - node-fetch: 2.7.0 - nopt: 5.0.0 - npmlog: 5.0.1 - rimraf: 3.0.2 - semver: 7.6.3 - tar: 6.2.1 - transitivePeerDependencies: - - encoding - - supports-color - dev: false - - /@types/bcrypt@5.0.2: - resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} - dependencies: - '@types/node': 22.5.5 - dev: true - - /@types/inquirer@9.0.7: - resolution: {integrity: sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==} - dependencies: - '@types/through': 0.0.33 - rxjs: 7.8.1 - dev: false - - /@types/js-yaml@4.0.9: - resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - dev: false - - /@types/node@22.5.5: - resolution: {integrity: sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==} - dependencies: - undici-types: 6.19.8 - - /@types/through@0.0.33: - resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} - dependencies: - '@types/node': 22.5.5 - dev: false - - /abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - dev: false - - /agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - dependencies: - debug: 4.3.7 - transitivePeerDependencies: - - supports-color - dev: false - - /ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.21.3 - dev: false - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: false - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: false - - /aproba@2.0.0: - resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} - dev: false - - /are-we-there-yet@2.0.0: - resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.2 - dev: false - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: false - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: false - - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: false - - /bcrypt@5.1.1: - resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} - engines: {node: '>= 10.0.0'} - requiresBuild: true - dependencies: - '@mapbox/node-pre-gyp': 1.0.11 - node-addon-api: 5.1.0 - transitivePeerDependencies: - - encoding - - supports-color - dev: false - - /bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: false - - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: false - - /buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: false - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: false - - /chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - dev: false - - /chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - dev: false - - /cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} - dependencies: - restore-cursor: 3.1.0 - dev: false - - /cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - dev: false - - /cli-width@4.1.0: - resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} - engines: {node: '>= 12'} - dev: false - - /clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - dev: false - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: false - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: false - - /color-support@1.1.3: - resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true - dev: false - - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: false - - /console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - dev: false - - /debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - dev: false - - /defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - dependencies: - clone: 1.0.4 - dev: false - - /delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - dev: false - - /detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - dev: false - - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - 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 - - /fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - dev: false - - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: false - - /gauge@3.0.2: - resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - dependencies: - aproba: 2.0.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - object-assign: 4.1.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - dev: false - - /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 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: false - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: false - - /has-unicode@2.0.1: - resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - dev: false - - /https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - dependencies: - agent-base: 6.0.2 - debug: 4.3.7 - transitivePeerDependencies: - - supports-color - dev: false - - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: false - - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: false - - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - dev: false - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: false - - /inquirer@9.3.6: - resolution: {integrity: sha512-riK/iQB2ctwkpWYgjjWIRv3MBLt2gzb2Sj0JNQNbyTXgyXsLWcDPJ5WS5ZDTCx7BRFnJsARtYh+58fjP5M2Y0Q==} - engines: {node: '>=18'} - dependencies: - '@inquirer/figures': 1.0.6 - 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 - - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: false - - /is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - dev: false - - /is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - dev: false - - /jiti@1.21.6: - resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} - hasBin: true - dev: false - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: false - - /log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - dev: false - - /make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - dependencies: - semver: 6.3.1 - dev: false - - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: false - - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: false - - /minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - dependencies: - yallist: 4.0.0 - dev: false - - /minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - dev: false - - /minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - dev: false - - /mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - dev: false - - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - 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 - - /node-addon-api@5.1.0: - resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} - dev: false - - /node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: false - - /nopt@5.0.0: - resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} - engines: {node: '>=6'} - hasBin: true - dependencies: - abbrev: 1.1.1 - dev: false - - /npmlog@5.0.1: - resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} - deprecated: This package is no longer supported. - dependencies: - are-we-there-yet: 2.0.0 - console-control-strings: 1.1.0 - gauge: 3.0.2 - set-blocking: 2.0.0 - dev: false - - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: false - - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - dev: false - - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - dependencies: - mimic-fn: 2.1.0 - dev: false - - /ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - dev: false - - /os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - dev: false - - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: false - - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - dev: false - - /restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - dev: false - - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - dependencies: - glob: 7.2.3 - dev: false - - /run-async@3.0.0: - resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} - engines: {node: '>=0.12.0'} - dev: false - - /rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - dependencies: - tslib: 2.7.0 - dev: false - - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: false - - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: false - - /semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - dev: false - - /semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - dev: false - - /set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: false - - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: false - - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: false - - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - dependencies: - safe-buffer: 5.2.1 - dev: false - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: false - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: false - - /tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - dev: false - - /tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - dependencies: - os-tmpdir: 1.0.2 - dev: false - - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: false - - /tslib@2.7.0: - resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} - dev: false - - /type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - dev: false - - /undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: false - - /wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - dependencies: - defaults: 1.0.4 - dev: false - - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: false - - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: false - - /wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - dependencies: - string-width: 4.2.3 - dev: false - - /wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: false - - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: false - - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: false - - /yoctocolors-cjs@2.1.2: - resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} - engines: {node: '>=18'} - dev: false diff --git a/self-hosting/quiz.ts b/self-hosting/quiz.ts index 70ab4fe2..7a7d4475 100644 --- a/self-hosting/quiz.ts +++ b/self-hosting/quiz.ts @@ -274,8 +274,9 @@ async function initiateOnboarding() { { type: 'input', name: 'CPUS', - default: os.cpus().length, - message: 'How many CPUs do you have?', + default: Math.max(Math.floor(os.cpus().length / 2), 1), + message: + 'How many workers do you want to spawn (in many cases 1-2 is enough)?', validate: (value) => { const parsed = Number.parseInt(value, 10); @@ -364,6 +365,7 @@ async function initiateOnboarding() { '\t- ./stop (example: ./stop)', '\t- ./logs (example: ./logs)', '\t- ./rebuild (example: ./rebuild op-dashboard)', + '\t- ./update (example: ./update) pulls the latest docker images and restarts the service', '', '2. Danger zone!', '\t- ./danger_wipe_everything (example: ./danger_wipe_everything)', diff --git a/self-hosting/setup b/self-hosting/setup index 36e549e9..a4157a70 100755 --- a/self-hosting/setup +++ b/self-hosting/setup @@ -12,12 +12,6 @@ install_nvm_and_node() { nvm use $NODE_VERSION } -# Function to install pnpm -install_pnpm() { - echo "Installing pnpm..." - npm install -g pnpm -} - # Function to install Docker install_docker() { echo "Installing Docker..." @@ -87,10 +81,5 @@ else fi -# Check if pnpm is installed -if ! command -v pnpm >/dev/null 2>&1; then - install_pnpm -fi - -pnpm --ignore-workspace install -./node_modules/.bin/jiti quiz.ts \ No newline at end of file +npm install +npm run quiz \ No newline at end of file diff --git a/self-hosting/update b/self-hosting/update new file mode 100755 index 00000000..c00736fa --- /dev/null +++ b/self-hosting/update @@ -0,0 +1,11 @@ +#!/bin/bash + +git pull + +echo "Pulling latest docker images" +docker compose pull + +echo "Restarting services" +docker compose restart + +echo "Done"