A discord bot for teal.fm
discord tealfm music

fmt

besaid.zone e479023e 17766f7a

verified
+4
.oxfmtrc.json
···
··· 1 + { 2 + "$schema": "./node_modules/oxfmt/configuration_schema.json", 3 + "ignorePatterns": [] 4 + }
+4 -4
PLAN.md
··· 1 # rough notes on how I think this should work 2 3 - we start of with no accounts 4 - - ``/teal auth`` sends user a link to log in with atproto account 5 - after auth success, we take did and send http request to tap instance to start backfilling for repo 6 - user can now use bot commands 7 8 ## planned commands 9 10 - - ``teal auth`` sends user a link to log in with atproto account 11 - - ``top <artist>``: top 10 listeners for artist (total amount of plays across all songs / albums) 12 - - ``recent``: most recent play 13 14 ## web interface for account management...maybe..? 15
··· 1 # rough notes on how I think this should work 2 3 - we start of with no accounts 4 + - `/teal auth` sends user a link to log in with atproto account 5 - after auth success, we take did and send http request to tap instance to start backfilling for repo 6 - user can now use bot commands 7 8 ## planned commands 9 10 + - `teal auth` sends user a link to log in with atproto account 11 + - `top <artist>`: top 10 listeners for artist (total amount of plays across all songs / albums) 12 + - `recent`: most recent play 13 14 ## web interface for account management...maybe..? 15
+6 -4
apps/bot/commands/auth.ts
··· 2 import { logger } from "@tealfmbot/common/logger.ts"; 3 4 export default { 5 - data: new SlashCommandBuilder().setName("auth").setDescription( 6 - "Authenticate your account with the teal.fm bot to start tracking your listens", 7 - ), 8 async execute(interaction: CommandInteraction) { 9 await interaction.reply("placeholder"); 10 - logger.info("auth command sent") 11 }, 12 };
··· 2 import { logger } from "@tealfmbot/common/logger.ts"; 3 4 export default { 5 + data: new SlashCommandBuilder() 6 + .setName("auth") 7 + .setDescription( 8 + "Authenticate your account with the teal.fm bot to start tracking your listens", 9 + ), 10 async execute(interaction: CommandInteraction) { 11 await interaction.reply("placeholder"); 12 + logger.info("auth command sent"); 13 }, 14 };
+2 -4
apps/bot/commands/ping.ts
··· 2 import { logger } from "@tealfmbot/common/logger.ts"; 3 4 export default { 5 - data: new SlashCommandBuilder().setName("ping").setDescription( 6 - "replies with pong", 7 - ), 8 async execute(interaction: CommandInteraction) { 9 await interaction.reply("pong lol"); 10 - logger.info("ping command sent") 11 }, 12 };
··· 2 import { logger } from "@tealfmbot/common/logger.ts"; 3 4 export default { 5 + data: new SlashCommandBuilder().setName("ping").setDescription("replies with pong"), 6 async execute(interaction: CommandInteraction) { 7 await interaction.reply("pong lol"); 8 + logger.info("ping command sent"); 9 }, 10 };
+4 -4
apps/bot/commands/top.ts
··· 2 import { logger } from "@tealfmbot/common/logger.ts"; 3 4 export default { 5 - data: new SlashCommandBuilder().setName("top").setDescription( 6 - "Find the top listeners for the specified artist(s)", 7 - ), 8 async execute(interaction: CommandInteraction) { 9 await interaction.reply("placeholder"); 10 - logger.info("top command sent") 11 }, 12 };
··· 2 import { logger } from "@tealfmbot/common/logger.ts"; 3 4 export default { 5 + data: new SlashCommandBuilder() 6 + .setName("top") 7 + .setDescription("Find the top listeners for the specified artist(s)"), 8 async execute(interaction: CommandInteraction) { 9 await interaction.reply("placeholder"); 10 + logger.info("top command sent"); 11 }, 12 };
+6 -10
apps/bot/deploy-commands.ts
··· 11 const commandPaths = fs.globSync("commands/**/*.ts"); 12 13 for await (const cmdPath of commandPaths) { 14 - const absoluteCommandPath = path.join(import.meta.dirname, cmdPath) 15 - const command = await import(absoluteCommandPath) 16 if ("data" in command.default && "execute" in command.default) { 17 commands.push(command.default.data); 18 } else { ··· 26 27 (async () => { 28 try { 29 - console.log( 30 - `Started refreshing ${commands.length} application (/) commands.`, 31 - ); 32 33 - const data = await rest.put( 34 Routes.applicationGuildCommands(DISCORD_APPLICATION_ID, DISCORD_GUILD_ID), 35 { body: commands }, 36 - ) as unknown[]; 37 38 - console.log( 39 - `Successfully reloaded ${data.length} application (/) commands.`, 40 - ); 41 } catch (error) { 42 console.error(error); 43 }
··· 11 const commandPaths = fs.globSync("commands/**/*.ts"); 12 13 for await (const cmdPath of commandPaths) { 14 + const absoluteCommandPath = path.join(import.meta.dirname, cmdPath); 15 + const command = await import(absoluteCommandPath); 16 if ("data" in command.default && "execute" in command.default) { 17 commands.push(command.default.data); 18 } else { ··· 26 27 (async () => { 28 try { 29 + console.log(`Started refreshing ${commands.length} application (/) commands.`); 30 31 + const data = (await rest.put( 32 Routes.applicationGuildCommands(DISCORD_APPLICATION_ID, DISCORD_GUILD_ID), 33 { body: commands }, 34 + )) as unknown[]; 35 36 + console.log(`Successfully reloaded ${data.length} application (/) commands.`); 37 } catch (error) { 38 console.error(error); 39 }
+1 -1
apps/bot/discord.d.ts
··· 2 3 declare module "discord.js" { 4 export interface Client { 5 - commands: Collection<unknown, any> 6 } 7 }
··· 2 3 declare module "discord.js" { 4 export interface Client { 5 + commands: Collection<unknown, any>; 6 } 7 }
+1 -7
apps/bot/main.ts
··· 1 - import { 2 - Client, 3 - Collection, 4 - Events, 5 - GatewayIntentBits, 6 - MessageFlags, 7 - } from "discord.js"; 8 import fs from "node:fs"; 9 import path from "node:path"; 10 import { DISCORD_BOT_TOKEN } from "@tealfmbot/common/constants.ts";
··· 1 + import { Client, Collection, Events, GatewayIntentBits, MessageFlags } from "discord.js"; 2 import fs from "node:fs"; 3 import path from "node:path"; 4 import { DISCORD_BOT_TOKEN } from "@tealfmbot/common/constants.ts";
+1 -1
apps/bot/package.json
··· 1 { 2 "name": "bot", 3 "version": "0.0.1", 4 - "type": "module", 5 "private": true, 6 "scripts": { 7 "dev": "tsx main.ts", 8 "deploy-commands": "tsx deploy-commands.ts",
··· 1 { 2 "name": "bot", 3 "version": "0.0.1", 4 "private": true, 5 + "type": "module", 6 "scripts": { 7 "dev": "tsx main.ts", 8 "deploy-commands": "tsx deploy-commands.ts",
+2 -4
apps/bot/tsconfig.json
··· 1 { 2 "extends": "@tealfmbot/tsconfig/tsconfig.node.json", 3 "compilerOptions": { 4 - "outDir": "./dist", 5 }, 6 - "exclude": [ 7 - "node_modules" 8 - ] 9 }
··· 1 { 2 "extends": "@tealfmbot/tsconfig/tsconfig.node.json", 3 "compilerOptions": { 4 + "outDir": "./dist" 5 }, 6 + "exclude": ["node_modules"] 7 }
+1 -2
apps/tapper/index.ts
··· 17 // track_name: evt?.record?.trackName, 18 // user_id: 4 19 // }).execute() 20 - console.log(evt.record) 21 } else { 22 console.log(`deleted: ${uri}`); 23 } 24 - 25 26 if (process.env.NODE_ENV === "development") { 27 // we don't want to ack in development
··· 17 // track_name: evt?.record?.trackName, 18 // user_id: 4 19 // }).execute() 20 + console.log(evt.record); 21 } else { 22 console.log(`deleted: ${uri}`); 23 } 24 25 if (process.env.NODE_ENV === "development") { 26 // we don't want to ack in development
+1 -1
apps/tapper/package.json
··· 1 { 2 "name": "tapper", 3 "version": "0.0.1", 4 - "type": "module", 5 "private": true, 6 "scripts": { 7 "dev": "NODE_ENV=development tsx index.ts", 8 "start": "NODE_ENV=production tsx index.ts",
··· 1 { 2 "name": "tapper", 3 "version": "0.0.1", 4 "private": true, 5 + "type": "module", 6 "scripts": { 7 "dev": "NODE_ENV=development tsx index.ts", 8 "start": "NODE_ENV=production tsx index.ts",
+1 -3
apps/tapper/tsconfig.json
··· 3 "compilerOptions": { 4 "outDir": "./dist" 5 }, 6 - "exclude": [ 7 - "node_modules" 8 - ] 9 }
··· 3 "compilerOptions": { 4 "outDir": "./dist" 5 }, 6 + "exclude": ["node_modules"] 7 }
+6
lefthook.yml
···
··· 1 + pre-commit: 2 + jobs: 3 + - name: format 4 + run: pnpm format {staged_files} 5 + exclude: 6 + - "**/node_modules"
+14 -11
package.json
··· 3 "version": "0.0.1", 4 "private": true, 5 "description": "A discord bot for teal.fm", 6 "scripts": { 7 "bot": "pnpm --filter bot dev", 8 "tap": "pnpm --filter tapper dev", 9 "dev": "pnpm --filter './apps/**' dev", 10 "typecheck": "pnpm --filter './apps/**' typecheck", 11 - "lint": "oxlint" 12 }, 13 - "keywords": [ 14 - "teal.fm", 15 - "atprotocol", 16 - "music" 17 - ], 18 - "author": "Dane Miller <me@dane.computer>", 19 - "license": "MIT", 20 - "packageManager": "pnpm@10.15.0", 21 - "repository": {}, 22 "devDependencies": { 23 "oxlint": "^1.35.0", 24 "typescript": "^5.9.3" 25 - } 26 }
··· 3 "version": "0.0.1", 4 "private": true, 5 "description": "A discord bot for teal.fm", 6 + "keywords": [ 7 + "atprotocol", 8 + "music", 9 + "teal.fm" 10 + ], 11 + "license": "MIT", 12 + "author": "Dane Miller <me@dane.computer>", 13 + "repository": {}, 14 "scripts": { 15 "bot": "pnpm --filter bot dev", 16 "tap": "pnpm --filter tapper dev", 17 "dev": "pnpm --filter './apps/**' dev", 18 "typecheck": "pnpm --filter './apps/**' typecheck", 19 + "lint": "oxlint", 20 + "format": "oxfmt" 21 }, 22 "devDependencies": { 23 + "lefthook": "^2.0.13", 24 + "oxfmt": "^0.20.0", 25 "oxlint": "^1.35.0", 26 "typescript": "^5.9.3" 27 + }, 28 + "packageManager": "pnpm@10.15.0" 29 }
+3 -3
packages/common/constants.ts
··· 1 - import { loadEnvFile } from "node:process" 2 - import path from "node:path" 3 4 - loadEnvFile(path.join(import.meta.dirname, "../../.env")) 5 6 export const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN as string; 7 export const DISCORD_APPLICATION_ID = process.env.DISCORD_APPLICATION_ID as string;
··· 1 + import { loadEnvFile } from "node:process"; 2 + import path from "node:path"; 3 4 + loadEnvFile(path.join(import.meta.dirname, "../../.env")); 5 6 export const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN as string; 7 export const DISCORD_APPLICATION_ID = process.env.DISCORD_APPLICATION_ID as string;
+5 -5
packages/common/logger.ts
··· 1 - import pino from "pino" 2 3 export const logger = pino({ 4 transport: { 5 target: "pino-pretty", 6 options: { 7 - colorize: true 8 - } 9 - } 10 - })
··· 1 + import pino from "pino"; 2 3 export const logger = pino({ 4 transport: { 5 target: "pino-pretty", 6 options: { 7 + colorize: true, 8 + }, 9 + }, 10 + });
+6 -6
packages/common/package.json
··· 1 { 2 "name": "@tealfmbot/common", 3 "version": "0.0.0", 4 - "type": "module", 5 "private": true, 6 "exports": { 7 "./*": "./*" 8 }, 9 "scripts": { 10 "typecheck": "tsc --noEmit" 11 }, 12 "devDependencies": { 13 "@tealfmbot/tsconfig": "workspace:*", 14 "@types/node": "^22.15.3", 15 - "typescript": "5.9.2", 16 - "pino-pretty": "^13.1.3" 17 - }, 18 - "dependencies": { 19 - "pino": "^10.1.0" 20 } 21 }
··· 1 { 2 "name": "@tealfmbot/common", 3 "version": "0.0.0", 4 "private": true, 5 + "type": "module", 6 "exports": { 7 "./*": "./*" 8 }, 9 "scripts": { 10 "typecheck": "tsc --noEmit" 11 }, 12 + "dependencies": { 13 + "pino": "^10.1.0" 14 + }, 15 "devDependencies": { 16 "@tealfmbot/tsconfig": "workspace:*", 17 "@types/node": "^22.15.3", 18 + "pino-pretty": "^13.1.3", 19 + "typescript": "5.9.2" 20 } 21 }
+1 -1
packages/common/tsconfig.json
··· 1 { 2 - "extends": "@tealfmbot/tsconfig/tsconfig.node.json", 3 }
··· 1 { 2 + "extends": "@tealfmbot/tsconfig/tsconfig.node.json" 3 }
+9 -9
packages/database/db.ts
··· 1 - import type { DB } from "kysely-codegen" 2 - import { Pool } from "pg" 3 - import { Kysely, PostgresDialect } from "kysely" 4 - import { DATABASE_URL } from "@tealfmbot/common/constants.ts" 5 6 const dialect = new PostgresDialect({ 7 pool: new Pool({ 8 - connectionString: DATABASE_URL 9 - }) 10 - }) 11 12 export const db = new Kysely<DB>({ 13 - dialect 14 - })
··· 1 + import type { DB } from "kysely-codegen"; 2 + import { Pool } from "pg"; 3 + import { Kysely, PostgresDialect } from "kysely"; 4 + import { DATABASE_URL } from "@tealfmbot/common/constants.ts"; 5 6 const dialect = new PostgresDialect({ 7 pool: new Pool({ 8 + connectionString: DATABASE_URL, 9 + }), 10 + }); 11 12 export const db = new Kysely<DB>({ 13 + dialect, 14 + });
+21 -16
packages/database/migrations/schema.ts
··· 1 - import { Kysely, sql } from "kysely" 2 3 export async function up(db: Kysely<any>): Promise<void> { 4 await db.schema 5 - .createTable("users").ifNotExists() 6 .addColumn("id", "serial", (col) => col.primaryKey()) 7 .addColumn("did", "varchar", (col) => col.notNull().unique()) 8 - .addColumn('created_at', 'timestamp', (col) => 9 - col.defaultTo(sql`now()`).notNull(), 10 - ) 11 - .execute() 12 13 await db.schema 14 - .createTable("plays").ifNotExists() 15 .addColumn("id", "serial", (col) => col.primaryKey()) 16 .addColumn("played_time", "timestamptz", (col) => col.notNull()) 17 .addColumn("release_name", "varchar", (col) => col.notNull()) 18 - .addColumn("track_name", 'varchar', (col) => col.notNull()) 19 .addColumn("indexed_at", "timestamp", (col) => col.defaultTo(sql`now()`).notNull()) 20 - .addColumn("user_id", "integer", (col) => col.references("users.id").onDelete("cascade").notNull()) 21 - .execute() 22 23 await db.schema 24 - .createTable("artists").ifNotExists() 25 .addColumn("artist_name", "varchar", (col) => col.notNull()) 26 - .addColumn("play_id", "integer", (col) => col.references("plays.id").onDelete("cascade").notNull()) 27 - .execute() 28 } 29 30 export async function down(db: Kysely<any>): Promise<void> { 31 - await db.schema.dropTable("users").execute() 32 - await db.schema.dropTable("plays").execute() 33 - await db.schema.dropTable("artists").execute() 34 }
··· 1 + import { Kysely, sql } from "kysely"; 2 3 export async function up(db: Kysely<any>): Promise<void> { 4 await db.schema 5 + .createTable("users") 6 + .ifNotExists() 7 .addColumn("id", "serial", (col) => col.primaryKey()) 8 .addColumn("did", "varchar", (col) => col.notNull().unique()) 9 + .addColumn("created_at", "timestamp", (col) => col.defaultTo(sql`now()`).notNull()) 10 + .execute(); 11 12 await db.schema 13 + .createTable("plays") 14 + .ifNotExists() 15 .addColumn("id", "serial", (col) => col.primaryKey()) 16 .addColumn("played_time", "timestamptz", (col) => col.notNull()) 17 .addColumn("release_name", "varchar", (col) => col.notNull()) 18 + .addColumn("track_name", "varchar", (col) => col.notNull()) 19 .addColumn("indexed_at", "timestamp", (col) => col.defaultTo(sql`now()`).notNull()) 20 + .addColumn("user_id", "integer", (col) => 21 + col.references("users.id").onDelete("cascade").notNull(), 22 + ) 23 + .execute(); 24 25 await db.schema 26 + .createTable("artists") 27 + .ifNotExists() 28 .addColumn("artist_name", "varchar", (col) => col.notNull()) 29 + .addColumn("play_id", "integer", (col) => 30 + col.references("plays.id").onDelete("cascade").notNull(), 31 + ) 32 + .execute(); 33 } 34 35 export async function down(db: Kysely<any>): Promise<void> { 36 + await db.schema.dropTable("users").execute(); 37 + await db.schema.dropTable("plays").execute(); 38 + await db.schema.dropTable("artists").execute(); 39 }
+23 -24
packages/database/migrator.ts
··· 1 - import path from "node:path" 2 - import { Pool } from "pg" 3 - import fs from "node:fs/promises" 4 - import type { DB } from "kysely-codegen" 5 - import { Kysely, Migrator, PostgresDialect, FileMigrationProvider } from "kysely" 6 - import { DATABASE_URL } from "@tealfmbot/common/constants.ts" 7 8 async function migrateToLatest() { 9 const db = new Kysely<DB>({ 10 dialect: new PostgresDialect({ 11 pool: new Pool({ 12 - connectionString: DATABASE_URL 13 - }) 14 - }) 15 - }) 16 17 const migrator = new Migrator({ 18 db, 19 provider: new FileMigrationProvider({ 20 fs, 21 path, 22 - migrationFolder: path.join(import.meta.dirname, "../migrations") 23 - }) 24 - }) 25 - 26 27 - const { error, results } = await migrator.migrateToLatest() 28 29 - results?.forEach(result => { 30 if (result.status === "Success") { 31 - console.log(`migration ${result.migrationName} was executed successfully`) 32 } else if (result.status === "Error") { 33 - console.error(`failed to execute migration: "${result.migrationName}" `) 34 } 35 - }) 36 37 if (error) { 38 - console.error("failed to migrate") 39 - console.error(error) 40 - process.exit(1) 41 } 42 43 - await db.destroy() 44 } 45 46 - migrateToLatest().catch(err => console.error(err))
··· 1 + import path from "node:path"; 2 + import { Pool } from "pg"; 3 + import fs from "node:fs/promises"; 4 + import type { DB } from "kysely-codegen"; 5 + import { Kysely, Migrator, PostgresDialect, FileMigrationProvider } from "kysely"; 6 + import { DATABASE_URL } from "@tealfmbot/common/constants.ts"; 7 8 async function migrateToLatest() { 9 const db = new Kysely<DB>({ 10 dialect: new PostgresDialect({ 11 pool: new Pool({ 12 + connectionString: DATABASE_URL, 13 + }), 14 + }), 15 + }); 16 17 const migrator = new Migrator({ 18 db, 19 provider: new FileMigrationProvider({ 20 fs, 21 path, 22 + migrationFolder: path.join(import.meta.dirname, "../migrations"), 23 + }), 24 + }); 25 26 + const { error, results } = await migrator.migrateToLatest(); 27 28 + results?.forEach((result) => { 29 if (result.status === "Success") { 30 + console.log(`migration ${result.migrationName} was executed successfully`); 31 } else if (result.status === "Error") { 32 + console.error(`failed to execute migration: "${result.migrationName}" `); 33 } 34 + }); 35 36 if (error) { 37 + console.error("failed to migrate"); 38 + console.error(error); 39 + process.exit(1); 40 } 41 42 + await db.destroy(); 43 } 44 45 + migrateToLatest().catch((err) => console.error(err));
+7 -7
packages/database/package.json
··· 1 { 2 "name": "@tealfmbot/database", 3 "version": "0.0.0", 4 - "type": "module", 5 "private": true, 6 "exports": { 7 "./db.ts": "./db.ts" 8 }, ··· 12 "seed": "tsx seed.ts", 13 "typecheck": "tsc --noEmit" 14 }, 15 "devDependencies": { 16 "@types/node": "^22.15.3", 17 "@types/pg": "^8.16.0", 18 "kysely-codegen": "^0.19.0", 19 "tsx": "^4.21.0", 20 "typescript": "5.9.2" 21 - }, 22 - "dependencies": { 23 - "@tealfmbot/common": "workspace:*", 24 - "@tealfmbot/tsconfig": "workspace:*", 25 - "kysely": "^0.28.9", 26 - "pg": "^8.16.3" 27 } 28 }
··· 1 { 2 "name": "@tealfmbot/database", 3 "version": "0.0.0", 4 "private": true, 5 + "type": "module", 6 "exports": { 7 "./db.ts": "./db.ts" 8 }, ··· 12 "seed": "tsx seed.ts", 13 "typecheck": "tsc --noEmit" 14 }, 15 + "dependencies": { 16 + "@tealfmbot/common": "workspace:*", 17 + "@tealfmbot/tsconfig": "workspace:*", 18 + "kysely": "^0.28.9", 19 + "pg": "^8.16.3" 20 + }, 21 "devDependencies": { 22 "@types/node": "^22.15.3", 23 "@types/pg": "^8.16.0", 24 "kysely-codegen": "^0.19.0", 25 "tsx": "^4.21.0", 26 "typescript": "5.9.2" 27 } 28 }
+1 -2
packages/database/seed.ts
··· 6 // }).returning(['did', 'created_at']) 7 // .executeTakeFirst() 8 // console.log({ result }) 9 - 10 // const users = await db.selectFrom("users").select(["id", "did"]).execute() 11 // console.log({ users }) 12 // await db.deleteFrom("users").where("did", "=", "did:plc:qttsv4e7pu2jl3ilanfgc3zn").execute() ··· 14 // console.log({ plays }) 15 } 16 17 - main().catch(error => console.error(error))
··· 6 // }).returning(['did', 'created_at']) 7 // .executeTakeFirst() 8 // console.log({ result }) 9 // const users = await db.selectFrom("users").select(["id", "did"]).execute() 10 // console.log({ users }) 11 // await db.deleteFrom("users").where("did", "=", "did:plc:qttsv4e7pu2jl3ilanfgc3zn").execute() ··· 13 // console.log({ plays }) 14 } 15 16 + main().catch((error) => console.error(error));
+1 -3
packages/database/tsconfig.json
··· 1 { 2 "extends": "@tealfmbot/tsconfig/tsconfig.node.json", 3 - "exclude": [ 4 - "node_modules" 5 - ] 6 }
··· 1 { 2 "extends": "@tealfmbot/tsconfig/tsconfig.node.json", 3 + "exclude": ["node_modules"] 4 }
+1 -1
packages/database/types.ts
··· 1 import type { Artists, Plays } from "kysely-codegen"; 2 3 export interface TealFMPlay extends Plays { 4 - artists: Artists 5 }
··· 1 import type { Artists, Plays } from "kysely-codegen"; 2 3 export interface TealFMPlay extends Plays { 4 + artists: Artists; 5 }
+1 -1
packages/tsconfig/tsconfig.base.json
··· 12 "allowImportingTsExtensions": true, 13 "noUncheckedSideEffectImports": true, 14 "moduleDetection": "force", 15 - "skipLibCheck": true, 16 } 17 }
··· 12 "allowImportingTsExtensions": true, 13 "noUncheckedSideEffectImports": true, 14 "moduleDetection": "force", 15 + "skipLibCheck": true 16 } 17 }
+2 -6
packages/tsconfig/tsconfig.node.json
··· 1 { 2 "extends": "./tsconfig.base.json", 3 "compilerOptions": { 4 - "lib": [ 5 - "esnext" 6 - ], 7 - "types": [ 8 - "node" 9 - ], 10 } 11 }
··· 1 { 2 "extends": "./tsconfig.base.json", 3 "compilerOptions": { 4 + "lib": ["esnext"], 5 + "types": ["node"] 6 } 7 }
+191
pnpm-lock.yaml
··· 8 9 .: 10 devDependencies: 11 oxlint: 12 specifier: ^1.35.0 13 version: 1.35.0 ··· 332 cpu: [x64] 333 os: [win32] 334 335 '@oxlint/darwin-arm64@1.35.0': 336 resolution: {integrity: sha512-ieiYVHkNZPo77Hgrxav595wGS4rRNKuDNrljf+4xhwpJsddrxMpM64IQUf2IvR3MhK4FxdGzhhB6OVmGVHY5/w==} 337 cpu: [arm64] ··· 670 resolution: {integrity: sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==} 671 engines: {node: '>=20.0.0'} 672 673 lines-and-columns@1.2.4: 674 resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 675 ··· 705 706 once@1.4.0: 707 resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 708 709 oxlint@1.35.0: 710 resolution: {integrity: sha512-QDX1aUgaiqznkGfTM2qHwva2wtKqhVoqPSVXrnPz+yLUhlNadikD3QRuRtppHl7WGuy3wG6nKAuR8lash3aWSg==} ··· 913 thread-stream@3.1.0: 914 resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} 915 916 to-regex-range@5.0.1: 917 resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 918 engines: {node: '>=8.0'} ··· 1171 '@esbuild/win32-x64@0.27.2': 1172 optional: true 1173 1174 '@oxlint/darwin-arm64@1.35.0': 1175 optional: true 1176 ··· 1481 1482 kysely@0.28.9: {} 1483 1484 lines-and-columns@1.2.4: {} 1485 1486 lodash.snakecase@4.1.1: {} ··· 1509 once@1.4.0: 1510 dependencies: 1511 wrappy: 1.0.2 1512 1513 oxlint@1.35.0: 1514 optionalDependencies: ··· 1732 thread-stream@3.1.0: 1733 dependencies: 1734 real-require: 0.2.0 1735 1736 to-regex-range@5.0.1: 1737 dependencies:
··· 8 9 .: 10 devDependencies: 11 + lefthook: 12 + specifier: ^2.0.13 13 + version: 2.0.13 14 + oxfmt: 15 + specifier: ^0.20.0 16 + version: 0.20.0 17 oxlint: 18 specifier: ^1.35.0 19 version: 1.35.0 ··· 338 cpu: [x64] 339 os: [win32] 340 341 + '@oxfmt/darwin-arm64@0.20.0': 342 + resolution: {integrity: sha512-bjR5dqvrd9gxKYfYR0ljUu3/T3+TuDVWcwA7d+tsfmx9lqidlw3zhgBTblnjF1mrd1zkPMoc5zzq86GeSEt1cA==} 343 + cpu: [arm64] 344 + os: [darwin] 345 + 346 + '@oxfmt/darwin-x64@0.20.0': 347 + resolution: {integrity: sha512-esUDes8FlJX3IY4TVjFLgZrnZlIIyPDlhkCaHgGR3+z2eHFZOvQu68kTSpZLCEJmGXdSpU5rlveycQ6n8tk9ew==} 348 + cpu: [x64] 349 + os: [darwin] 350 + 351 + '@oxfmt/linux-arm64-gnu@0.20.0': 352 + resolution: {integrity: sha512-irE0RO9B0R6ziQE6kUVZtZ6IuTdRyuumn1cPWhDfpa0XUa5sE0ly8pjVsvJbj/J9qerVtidU05txeXBB5CirQg==} 353 + cpu: [arm64] 354 + os: [linux] 355 + 356 + '@oxfmt/linux-arm64-musl@0.20.0': 357 + resolution: {integrity: sha512-eXPBLwYJm26DCmwMwhelEwQMRwuGNaYhYZOhd+CYYsmVoF+h6L6dtjwj0Ovuu0Gqh18EL8vfsaoUvb+jr3vEBg==} 358 + cpu: [arm64] 359 + os: [linux] 360 + 361 + '@oxfmt/linux-x64-gnu@0.20.0': 362 + resolution: {integrity: sha512-dTPW38Hjgb7LoD2mNgyQGBaJ1hu5YgPrxImhl5Eb04eiws+ETCM0wrb2TWGduA+Nv3rHKn3vZEkMTEjklZXgRw==} 363 + cpu: [x64] 364 + os: [linux] 365 + 366 + '@oxfmt/linux-x64-musl@0.20.0': 367 + resolution: {integrity: sha512-b4duw9JGDK/kZoqrPNU9tBOOZQdUW8KJPZ7gW7z54X1eGSqCJ1PT0XLNmZ7SOA1BzQwQ0a3qmQWfFVOsH3a5bw==} 368 + cpu: [x64] 369 + os: [linux] 370 + 371 + '@oxfmt/win32-arm64@0.20.0': 372 + resolution: {integrity: sha512-XAzvBhw4K+Fe16dBaFgYAdob9WaM8RYEXl0ibbm5NlNaQEq+5bH9xwc0oaYlHFnLfcgXWmn9ceTAYqNlONQRNA==} 373 + cpu: [arm64] 374 + os: [win32] 375 + 376 + '@oxfmt/win32-x64@0.20.0': 377 + resolution: {integrity: sha512-fkJqHbJaoOMRmrjHSljyb4/7BgXO3xPLBsJSFGtm3mpfW0HHFbAKvd4/6njhqJz9KY+b3RWP1WssjFshcqQQ4w==} 378 + cpu: [x64] 379 + os: [win32] 380 + 381 '@oxlint/darwin-arm64@1.35.0': 382 resolution: {integrity: sha512-ieiYVHkNZPo77Hgrxav595wGS4rRNKuDNrljf+4xhwpJsddrxMpM64IQUf2IvR3MhK4FxdGzhhB6OVmGVHY5/w==} 383 cpu: [arm64] ··· 716 resolution: {integrity: sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==} 717 engines: {node: '>=20.0.0'} 718 719 + lefthook-darwin-arm64@2.0.13: 720 + resolution: {integrity: sha512-KbQqpNSNTugjtPzt97CNcy/XZy5asJ0+uSLoHc4ML8UCJdsXKYJGozJHNwAd0Xfci/rQlj82A7rPOuTdh0jY0Q==} 721 + cpu: [arm64] 722 + os: [darwin] 723 + 724 + lefthook-darwin-x64@2.0.13: 725 + resolution: {integrity: sha512-s/vI6sEE8/+rE6CONZzs59LxyuSc/KdU+/3adkNx+Q13R1+p/AvQNeszg3LAHzXmF3NqlxYf8jbj/z5vBzEpRw==} 726 + cpu: [x64] 727 + os: [darwin] 728 + 729 + lefthook-freebsd-arm64@2.0.13: 730 + resolution: {integrity: sha512-iQeJTU7Zl8EJlCMQxNZQpJFAQ9xl40pydUIv5SYnbJ4nqIr9ONuvrioNv6N2LtKP5aBl1nIWQQ9vMjgVyb3k+A==} 731 + cpu: [arm64] 732 + os: [freebsd] 733 + 734 + lefthook-freebsd-x64@2.0.13: 735 + resolution: {integrity: sha512-99cAXKRIzpq/u3obUXbOQJCHP+0ZkJbN3TF+1ZQZlRo3Y6+mPSCg9fh/oi6dgbtu4gTI5Ifz3o5p2KZzAIF9ZQ==} 736 + cpu: [x64] 737 + os: [freebsd] 738 + 739 + lefthook-linux-arm64@2.0.13: 740 + resolution: {integrity: sha512-RWarenY3kLy/DT4/8dY2bwDlYwlELRq9MIFq+FiMYmoBHES3ckWcLX2JMMlM49Y672paQc7MbneSrNUn/FQWhg==} 741 + cpu: [arm64] 742 + os: [linux] 743 + 744 + lefthook-linux-x64@2.0.13: 745 + resolution: {integrity: sha512-QZRcxXGf8Uj/75ITBqoBh0zWhJE7+uFoRxEHwBq0Qjv55Q4KcFm7FBN/IFQCSd14reY5pmY3kDaWVVy60cAGJA==} 746 + cpu: [x64] 747 + os: [linux] 748 + 749 + lefthook-openbsd-arm64@2.0.13: 750 + resolution: {integrity: sha512-LAuOWwnNmOlRE0RxKMOhIz5Kr9tXi0rCjzXtDARW9lvfAV6Br2wP+47q0rqQ8m/nVwBYoxfJ/RDunLbb86O1nA==} 751 + cpu: [arm64] 752 + os: [openbsd] 753 + 754 + lefthook-openbsd-x64@2.0.13: 755 + resolution: {integrity: sha512-n9TIN3QLncyxOHomiKKwzDFHKOCm5H28CVNAZFouKqDwEaUGCs5TJI88V85j4/CgmLVUU8uUn4ClVCxIWYG59w==} 756 + cpu: [x64] 757 + os: [openbsd] 758 + 759 + lefthook-windows-arm64@2.0.13: 760 + resolution: {integrity: sha512-sdSC4F9Di7y0t43Of9MOA5g/0CmvkM4juQ3sKfUhRcoygetLJn4PR2/pvuDOIaGf4mNMXBP5IrcKaeDON9HrcA==} 761 + cpu: [arm64] 762 + os: [win32] 763 + 764 + lefthook-windows-x64@2.0.13: 765 + resolution: {integrity: sha512-ccl1v7Fl10qYoghEtjXN+JC1x/y/zLM/NSHf3NFGeKEGBNd1P5d/j6w8zVmhfzi+ekS8whXrcNbRAkLdAqUrSw==} 766 + cpu: [x64] 767 + os: [win32] 768 + 769 + lefthook@2.0.13: 770 + resolution: {integrity: sha512-D39rCVl7/GpqakvhQvqz07SBpzUWTvWjXKnBZyIy8O6D+Lf9xD6tnbHtG5nWXd9iPvv1AKGQwL9R/e5rNtV6SQ==} 771 + hasBin: true 772 + 773 lines-and-columns@1.2.4: 774 resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 775 ··· 805 806 once@1.4.0: 807 resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 808 + 809 + oxfmt@0.20.0: 810 + resolution: {integrity: sha512-+7f8eV8iaK3tENN/FUVxZM1g78HjPehybN8/+/dvEA1O893Dcvk6O7/Q1wTQOHMD7wvdwWdujKl+Uo8QMiKDrQ==} 811 + engines: {node: ^20.19.0 || >=22.12.0} 812 + hasBin: true 813 814 oxlint@1.35.0: 815 resolution: {integrity: sha512-QDX1aUgaiqznkGfTM2qHwva2wtKqhVoqPSVXrnPz+yLUhlNadikD3QRuRtppHl7WGuy3wG6nKAuR8lash3aWSg==} ··· 1018 thread-stream@3.1.0: 1019 resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} 1020 1021 + tinypool@2.0.0: 1022 + resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==} 1023 + engines: {node: ^20.0.0 || >=22.0.0} 1024 + 1025 to-regex-range@5.0.1: 1026 resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1027 engines: {node: '>=8.0'} ··· 1280 '@esbuild/win32-x64@0.27.2': 1281 optional: true 1282 1283 + '@oxfmt/darwin-arm64@0.20.0': 1284 + optional: true 1285 + 1286 + '@oxfmt/darwin-x64@0.20.0': 1287 + optional: true 1288 + 1289 + '@oxfmt/linux-arm64-gnu@0.20.0': 1290 + optional: true 1291 + 1292 + '@oxfmt/linux-arm64-musl@0.20.0': 1293 + optional: true 1294 + 1295 + '@oxfmt/linux-x64-gnu@0.20.0': 1296 + optional: true 1297 + 1298 + '@oxfmt/linux-x64-musl@0.20.0': 1299 + optional: true 1300 + 1301 + '@oxfmt/win32-arm64@0.20.0': 1302 + optional: true 1303 + 1304 + '@oxfmt/win32-x64@0.20.0': 1305 + optional: true 1306 + 1307 '@oxlint/darwin-arm64@1.35.0': 1308 optional: true 1309 ··· 1614 1615 kysely@0.28.9: {} 1616 1617 + lefthook-darwin-arm64@2.0.13: 1618 + optional: true 1619 + 1620 + lefthook-darwin-x64@2.0.13: 1621 + optional: true 1622 + 1623 + lefthook-freebsd-arm64@2.0.13: 1624 + optional: true 1625 + 1626 + lefthook-freebsd-x64@2.0.13: 1627 + optional: true 1628 + 1629 + lefthook-linux-arm64@2.0.13: 1630 + optional: true 1631 + 1632 + lefthook-linux-x64@2.0.13: 1633 + optional: true 1634 + 1635 + lefthook-openbsd-arm64@2.0.13: 1636 + optional: true 1637 + 1638 + lefthook-openbsd-x64@2.0.13: 1639 + optional: true 1640 + 1641 + lefthook-windows-arm64@2.0.13: 1642 + optional: true 1643 + 1644 + lefthook-windows-x64@2.0.13: 1645 + optional: true 1646 + 1647 + lefthook@2.0.13: 1648 + optionalDependencies: 1649 + lefthook-darwin-arm64: 2.0.13 1650 + lefthook-darwin-x64: 2.0.13 1651 + lefthook-freebsd-arm64: 2.0.13 1652 + lefthook-freebsd-x64: 2.0.13 1653 + lefthook-linux-arm64: 2.0.13 1654 + lefthook-linux-x64: 2.0.13 1655 + lefthook-openbsd-arm64: 2.0.13 1656 + lefthook-openbsd-x64: 2.0.13 1657 + lefthook-windows-arm64: 2.0.13 1658 + lefthook-windows-x64: 2.0.13 1659 + 1660 lines-and-columns@1.2.4: {} 1661 1662 lodash.snakecase@4.1.1: {} ··· 1685 once@1.4.0: 1686 dependencies: 1687 wrappy: 1.0.2 1688 + 1689 + oxfmt@0.20.0: 1690 + dependencies: 1691 + tinypool: 2.0.0 1692 + optionalDependencies: 1693 + '@oxfmt/darwin-arm64': 0.20.0 1694 + '@oxfmt/darwin-x64': 0.20.0 1695 + '@oxfmt/linux-arm64-gnu': 0.20.0 1696 + '@oxfmt/linux-arm64-musl': 0.20.0 1697 + '@oxfmt/linux-x64-gnu': 0.20.0 1698 + '@oxfmt/linux-x64-musl': 0.20.0 1699 + '@oxfmt/win32-arm64': 0.20.0 1700 + '@oxfmt/win32-x64': 0.20.0 1701 1702 oxlint@1.35.0: 1703 optionalDependencies: ··· 1921 thread-stream@3.1.0: 1922 dependencies: 1923 real-require: 0.2.0 1924 + 1925 + tinypool@2.0.0: {} 1926 1927 to-regex-range@5.0.1: 1928 dependencies: