10 Commits
23 changed files with 645 additions and 36 deletions
+20
View File
@@ -57,3 +57,23 @@ this directory:
Deployed to Fly.io (`fly.toml`). After a domain changes, re-run
`src/scripts/register-webhooks.ts` with production env vars to point Printful's
and Webflow's webhooks at the new domain — see issue #3 for the full checklist.
### Running migrations/seed against production
The deployed image only contains the compiled binary (see `Dockerfile`) — no
source, no `bun_modules`, no migration files — so these can't be run from
`fly ssh console` on the API app itself. Instead, use `migrate.sh` at the repo
root, which tunnels to the Postgres app (`blade-and-brawn-db`, legacy/unmanaged
Fly Postgres) via `fly proxy`, fetches the production `DATABASE_URL` for you,
and runs the scripts against it:
```bash
./migrate.sh migrate # db:migrate:latest (default if no argument given)
./migrate.sh seed # db:seed
./migrate.sh both # both, in order
```
Both underlying scripts prompt for a `y/N` confirmation before touching the
database, and `db:seed` is idempotent (skips seeding if the default rows
already exist). If the tunneled connection fails on TLS, legacy Postgres
sometimes needs `?sslmode=disable` appended — edit `migrate.sh` if so.
+3 -3
View File
@@ -27,11 +27,11 @@
"@elysia/jwt": "^1.4.2",
"@elysia/server-timing": "^1.4.1",
"@elysiajs/cors": "^1.4.2",
"@types/pg": "^8.20.0",
"@types/pg": "^8.21.0",
"elysia": "^1.4.29",
"kysely": "^0.29.4",
"kysely": "^0.29.5",
"ml-levenberg-marquardt": "^5.1.0",
"pg": "^8.22.0",
"pg": "^8.23.0",
"zipcodes-us": "^1.1.3"
},
"devDependencies": {
@@ -0,0 +1,47 @@
import { Kysely, sql } from 'kysely'
import { addDefaultColumns } from '../db';
export async function up(db: Kysely<any>): Promise<void> {
// TABLE: ACCOUNTS
await db.schema.createTable("accounts")
.addColumn("id", "uuid", (cb) => cb.primaryKey().defaultTo(sql`uuidv7()`))
.addColumn("created_at", "timestamptz", (cb) => cb
.notNull()
.defaultTo(sql`now()`)
)
.addColumn("discord_id", "text", (cb) => cb.unique())
.addColumn("email", "text", (cb) => cb.unique())
.addColumn("name", "text", (cb) => cb.notNull().defaultTo("Anonymous"))
.addColumn("gender", "text")
.execute();
// TABLE: ASSESSMENTS
await db.schema.createTable("assessments")
.$call(addDefaultColumns)
.addColumn("account_id", "uuid")
.addColumn("name", "text", (cb) => cb.notNull().defaultTo("Anonymous"))
.addColumn("gender", "text", (cb) => cb.notNull())
.addColumn("age", "integer", (cb) => cb.notNull())
.addColumn("weight", "float8", (cb) => cb.notNull())
.addColumn("perf_back_squat", "float8") // kg
.addColumn("perf_deadlift", "float8") // kg
.addColumn("perf_bench_press", "float8") // kg
.addColumn("perf_run", "float8") // ms
.addColumn("perf_broad_jump", "float8") // cm
.addColumn("perf_cone_drill", "float8") // ms
.addForeignKeyConstraint(
"fk_assessments_account_id",
["account_id"],
"accounts",
["id"],
(cb) => cb.onDelete("cascade")
)
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
// TABLE: ASSESSMENTS
await db.schema.dropTable("assessments").ifExists().execute()
// TABLE: ACCOUNTS
await db.schema.dropTable("accounts").ifExists().execute()
}
+42
View File
@@ -1,6 +1,7 @@
import {
StandardsConfigSchema,
} from "@blade-and-brawn/calculator";
import { Gender } from "@blade-and-brawn/domain";
import { Value } from "@sinclair/typebox/value";
import { db } from "./db";
import { DEFAULT_NAME, env, log } from "../util";
@@ -68,6 +69,46 @@ async function seedCalculator(standardsConfigId: string): Promise<void> {
log.info({ id: result.id }, "seeded default calculator");
}
async function seedAccounts(): Promise<void> {
const ACCOUNT_NAME = "Xominus";
const existing = await db.selectFrom("accounts")
.select(["id"])
.where("name", "=", ACCOUNT_NAME)
.executeTakeFirst();
if (existing) {
log.info({ id: existing.id }, "default account already seeded, skipping");
return;
}
const account = await db.insertInto("accounts")
.values({
name: ACCOUNT_NAME,
discord_id: "1325552430942916731",
email: "xominus@bladeandbrawn.com"
})
.returning("id")
.executeTakeFirstOrThrow();
log.info({ id: account.id }, "seeded default account");
await db.insertInto("assessments")
.values({
account_id: account.id,
name: ACCOUNT_NAME,
gender: Gender.Male,
age: 25,
weight: 80,
perf_back_squat: 72.57472,
perf_deadlift: 127.00576,
perf_bench_press: 102.0582,
perf_run: 446000,
perf_broad_jump: 185.42000000000002,
perf_cone_drill: 9870,
})
.execute();
log.info({ accountId: account.id }, "seeded placeholder assessment");
}
(async () => {
const answer = prompt(`Seed the database (${env.DATABASE_URL}) with default standards data? (y/N)`);
if (answer?.trim().toLowerCase() !== "y") {
@@ -80,6 +121,7 @@ async function seedCalculator(standardsConfigId: string): Promise<void> {
const datasetId = await seedStandardsDataset();
const standardsConfigId = await seedStandardsConfig(datasetId);
await seedCalculator(standardsConfigId);
await seedAccounts();
log.info("seed finished");
}
catch (err) {
+25 -2
View File
@@ -20,10 +20,11 @@ import { CalculatorService, CalculatorUnavailableError } from "./services/calcul
import { StandardsParamsSchema } from "@blade-and-brawn/calculator";
import { StandardsService } from "./services/standards";
import { EventsService, EventStatusSchema } from "./services/events";
import { AccountsService } from "./services/accounts";
// CONSTANTS
// -----------------------
const EVENT_QUEUE_MANAGE_DELAY_MS = 1000 // 1 sec
const EVENT_QUEUE_MANAGE_DELAY_MS = 1000 // 1 sec
const JWT_EXP = "1d";
// SERVICES
@@ -32,8 +33,9 @@ const s = (() => {
const Standards = new StandardsService();
const Calculator = new CalculatorService(DEFAULT_NAME);
const Commerce = new CommerceService();
const Accounts = new AccountsService(Calculator);
const Events = new EventsService();
return { Standards, Calculator, Commerce, Events };
return { Standards, Calculator, Commerce, Accounts, Events };
})();
// QUEUES
@@ -337,6 +339,27 @@ export const app = new Elysia()
.get("/groups", async ({ }) => queues.map((q) => q.group))
)
// ACCOUNTS
.group("/accounts", (app) => app
// Non-authenticated
.get("/:id/stats", async ({ params: { id } }) => {
const stats = await s.Accounts.stats(id);
if (!stats) throw new NotFoundError("Account stats not found");
return stats;
}, {
params: t.Object({ id: t.String() })
})
// Authenticated
.guard({ auth: true })
.get("/:id", async ({ params: { id } }) => {
const account = await s.Accounts.get(id);
if (!account) throw new NotFoundError("Account not found");
return account;
}, {
params: t.Object({ id: t.String() })
})
)
// WEBHOOKS
.post("/webhooks/printful", async ({ body, query }) => {
// https://webflow.com/integrations/printful
+79
View File
@@ -0,0 +1,79 @@
import { Activity, Gender, type Player } from "@blade-and-brawn/domain";
import { db } from "../database/db";
import type { CalculatorService } from "./calculator";
import { Value } from "@sinclair/typebox/value";
import { t } from "elysia";
export class AccountsService {
constructor(private Calculator: CalculatorService) { }
private static idComparison(id: string): ["accounts.id" | "accounts.discord_id", "=", string] {
return id.startsWith("@")
? ["accounts.discord_id", "=", id.slice(1)]
: ["accounts.id", "=", id];
}
async get(id: string) {
return await (db).selectFrom("accounts")
.selectAll()
.where(...AccountsService.idComparison(id))
.executeTakeFirst();
}
async stats(id: string) {
const latestAssessment = await db.selectFrom("accounts")
.innerJoin("assessments", "assessments.account_id", "accounts.id")
.select([
"assessments.age", "assessments.gender", "assessments.weight", "assessments.name",
"assessments.perf_back_squat", "assessments.perf_bench_press", "assessments.perf_broad_jump",
"assessments.perf_cone_drill", "assessments.perf_deadlift", "assessments.perf_run"
])
.where(...AccountsService.idComparison(id))
.orderBy("assessments.created_at", "desc")
.limit(1)
.executeTakeFirst();
if (!latestAssessment) return;
Value.Assert(t.Enum(Gender), latestAssessment.gender);
return this.Calculator.calculate(
{
name: latestAssessment.name,
metrics: {
age: latestAssessment.age,
weight: latestAssessment.weight,
gender: latestAssessment.gender
}
},
[
// Strength
{
activity: Activity.BenchPress,
performance: latestAssessment.perf_bench_press ?? 0
},
{
activity: Activity.Deadlift,
performance: latestAssessment.perf_deadlift ?? 0
},
{
activity: Activity.BackSquat,
performance: latestAssessment.perf_back_squat ?? 0
},
// Power
{
activity: Activity.BroadJump,
performance: latestAssessment.perf_broad_jump ?? 0
},
// Endurance
{
activity: Activity.Run,
performance: latestAssessment.perf_run ?? 0
},
// Agility
{
activity: Activity.ConeDrill,
performance: latestAssessment.perf_cone_drill ?? 0
},
]
);
}
}
View File
+106
View File
@@ -0,0 +1,106 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
import { createRoot } from "react-dom/client";
// import .css files directly and it works
import './index.css';
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
+15
View File
@@ -0,0 +1,15 @@
# bot
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@blade-and-brawn/bot",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@blade-and-brawn/api": "workspace:*"
},
"peerDependencies": {},
"dependencies": {
"@elysia/eden": "^1.4.10",
"discord.js": "^14.27.0"
},
"scripts": {
"dev": "bun run src/index.ts"
}
}
+32
View File
@@ -0,0 +1,32 @@
import { Client, Events, GatewayIntentBits } from 'discord.js';
import { CommandService } from './services/cmd/service';
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent]
});
// SERVICES
// -----------------------
const s = (() => {
const Commands = new CommandService(client);
return { Commands };
})();
client.once(Events.ClientReady, async (readyClient) => {
// Initialize
await s.Commands.init();
console.log(`Ready! Logged in as ${readyClient.user.tag}`);
});
client.on("guildMemberAdd", async (guildMember) => {
});
client.on("messageCreate", async (message) => {
if (message.author.bot) return;
const command = s.Commands.parse(message);
if (command) command.execute(message);
});
client.login(Bun.env.BOT_TOKEN);
@@ -0,0 +1,41 @@
import { EmbedBuilder, type Message } from "discord.js";
import type { Command } from "../service";
import { api } from "../../../util";
const ATTRIBUTE_EMOJI: Record<string, string> = {
Strength: "💪",
Power: "⚡",
Endurance: "🏃",
Agility: "🤸",
};
export default {
name: "stats",
description: "View your fitness statistics!",
execute: async (message: Message) => {
const res = await api.accounts({ id: `@${message.author.id}` }).stats.get();
if (!res.data) {
await message.reply("No stats found yet — submit an assessment first!");
return;
}
const { player, attributes } = res.data;
const embed = new EmbedBuilder()
.setColor(0xE74C3C)
.setTitle(`${message.author.username}'s Stats`)
.setThumbnail(message.author.displayAvatarURL())
.setDescription(`**Overall Level: ${player}**`)
.addFields(
Object.entries(attributes).map(([attribute, level]) => ({
name: `${ATTRIBUTE_EMOJI[attribute] ?? ""} ${attribute}`,
value: `Level ${level}`,
inline: true,
}))
)
.setFooter({ text: "Blade & Brawn" })
.setTimestamp();
await message.reply({ embeds: [embed] });
}
} as Command;
+33
View File
@@ -0,0 +1,33 @@
import type { Client, Message } from "discord.js";
import { readdir } from "fs/promises";
const CMD_PREFIX = ".";
export interface Command {
name: string,
description: string,
execute: (message: Message) => void | Promise<void>
}
export class CommandService {
private client: Client
private registry: Record<string, Command> = {}
constructor(client: Client) {
this.client = client;
}
async init() {
const commands = await Promise.all(
(await readdir(`${import.meta.dir}/registry`)).map(async file => (await import(`./registry/${file}`)).default as Command)
);
for (const command of commands)
this.registry[command.name] = command;
};
parse(message: Message): Command | undefined {
if (!message.content.startsWith(CMD_PREFIX)) return;
const name = message.content.slice(CMD_PREFIX.length).split(/\s+/)[0];
return this.registry[name ?? ""];
}
}
+38
View File
@@ -0,0 +1,38 @@
import cluster from "node:cluster";
import { pino } from "pino";
import { treaty } from '@elysia/eden';
import { type API } from "@blade-and-brawn/api";
export const log = pino({
level: Bun.env.LOG_LEVEL ?? "info",
transport: Bun.env.NODE_ENV != "production"
? { target: "pino-pretty" }
: undefined,
});
export const env = {
PUBLIC_API_URL: requireEnv("PUBLIC_API_URL"),
BOT_TOKEN: requireEnv("BOT_TOKEN"),
};
export const api = treaty<API>(env.PUBLIC_API_URL ?? "");
function requireEnv(key: string): string {
const val = Bun.env[key];
if (!val) {
if (cluster.worker?.id === 1)
log.error({ name: key }, "Missing required environment variable");
throw new Error(`Missing required environment variable: ${key}`)
};
return val;
}
function optionEnv(key: string, fallback: string = ""): string {
const val = Bun.env[key];
if (!val) {
if (cluster.worker?.id === 1)
log.warn({ name: key }, `Missing optional environmental variable, falling back to "${fallback}"`)
return fallback
};
return val;
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.base.json"
}
+4 -5
View File
@@ -5,11 +5,10 @@
"@blade-and-brawn/api": "workspace:*",
"@sveltejs/kit": "^2.70.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/bun": "^1.3.14",
"elysia": "^1.4.29",
"svelte": "^5.56.8",
"svelte": "^5.56.9",
"svelte-adapter-bun": "^1.0.1",
"svelte-check": "^4.7.4",
"svelte-check": "^4.7.6",
"vite": "^7.3.6"
},
"scripts": {
@@ -26,8 +25,8 @@
"@blade-and-brawn/domain": "workspace:*",
"@elysia/eden": "^1.4.10",
"@tailwindcss/vite": "^4.3.3",
"daisyui": "^5.7.9",
"jose": "^6.2.5",
"daisyui": "^5.7.17",
"jose": "^6.2.9",
"tailwindcss": "^4.3.3"
}
}
+76 -19
View File
@@ -22,11 +22,11 @@
"@elysia/jwt": "^1.4.2",
"@elysia/server-timing": "^1.4.1",
"@elysiajs/cors": "^1.4.2",
"@types/pg": "^8.20.0",
"@types/pg": "^8.21.0",
"elysia": "^1.4.29",
"kysely": "^0.29.4",
"kysely": "^0.29.5",
"ml-levenberg-marquardt": "^5.1.0",
"pg": "^8.22.0",
"pg": "^8.23.0",
"zipcodes-us": "^1.1.3",
},
"devDependencies": {
@@ -34,6 +34,16 @@
"pino-pretty": "^13.1.3",
},
},
"apps/bot": {
"name": "@blade-and-brawn/bot",
"dependencies": {
"@elysia/eden": "^1.4.10",
"discord.js": "^14.27.0",
},
"devDependencies": {
"@blade-and-brawn/api": "workspace:*",
},
},
"apps/portal": {
"name": "@blade-and-brawn/portal",
"dependencies": {
@@ -42,25 +52,24 @@
"@blade-and-brawn/domain": "workspace:*",
"@elysia/eden": "^1.4.10",
"@tailwindcss/vite": "^4.3.3",
"daisyui": "^5.7.9",
"jose": "^6.2.5",
"daisyui": "^5.7.17",
"jose": "^6.2.9",
"tailwindcss": "^4.3.3",
},
"devDependencies": {
"@blade-and-brawn/api": "workspace:*",
"@sveltejs/kit": "^2.70.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/bun": "^1.3.14",
"elysia": "^1.4.29",
"svelte": "^5.56.8",
"svelte": "^5.56.9",
"svelte-adapter-bun": "^1.0.1",
"svelte-check": "^4.7.4",
"svelte-check": "^4.7.6",
"vite": "^7.3.6",
},
},
"packages/calculator": {
"name": "@blade-and-brawn/calculator",
"version": "0.0.0",
"version": "1.0.0",
"dependencies": {
"@blade-and-brawn/domain": "workspace:*",
"ml-levenberg-marquardt": "^5.0.1",
@@ -68,14 +77,14 @@
},
"packages/commerce": {
"name": "@blade-and-brawn/commerce",
"version": "0.0.0",
"version": "1.0.0",
"dependencies": {
"@blade-and-brawn/domain": "workspace:*",
},
},
"packages/domain": {
"name": "@blade-and-brawn/domain",
"version": "0.0.0",
"version": "1.0.0",
"dependencies": {
"@blade-and-brawn/domain": "workspace:*",
},
@@ -88,6 +97,8 @@
"@blade-and-brawn/api": ["@blade-and-brawn/api@workspace:apps/api"],
"@blade-and-brawn/bot": ["@blade-and-brawn/bot@workspace:apps/bot"],
"@blade-and-brawn/calculator": ["@blade-and-brawn/calculator@workspace:packages/calculator"],
"@blade-and-brawn/commerce": ["@blade-and-brawn/commerce@workspace:packages/commerce"],
@@ -98,6 +109,18 @@
"@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
"@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="],
"@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
"@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="],
"@discordjs/rest": ["@discordjs/rest@2.6.3", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.50", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg=="],
"@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="],
"@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
"@elysia/eden": ["@elysia/eden@1.4.10", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-vcZXQcW6wZj6rhTxaiTkuCbVuS/yAJQ9jqCM6b83a5hu99F5Aj3HOyWXL17iod2Rz+pJpYsQGX0XaeoCsMpw2g=="],
"@elysia/jwt": ["@elysia/jwt@1.4.2", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.4.27" } }, "sha512-KhLe6XxI+NHoYZiHqPhWg8SU0v+pegsxXsZSa/CXic/BrjlFbO26uTERtOQNlnByp/IGUB4qpLF6kQhvfYvGyg=="],
@@ -264,6 +287,12 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.52", "", {}, "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -272,7 +301,7 @@
"@sveltejs/kit": ["@sveltejs/kit@2.70.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-RzRoRpuR2KXqc5yMO0akQHDZeT4AslOlznGITURsqHaVbtyYP4Wn3eE3gxj9JcDyNYO0crkxhdwFHc+2vkVm6w=="],
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.1", "", {}, "sha512-5m3B2cbqQ4TbwW6Xkh66Ntw6dD7gNc77cCxABTTesWcq9jxIzMgTk97pZx5vEtvQx8iokgi7GIphqZe+PGwcZA=="],
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
@@ -322,10 +351,12 @@
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
"@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
"@types/pg": ["@types/pg@8.21.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-AYdtudzabjLZgVgRZmAnU8bAnVUXzuJX2IYHeSIiIHm68olD+LgQYCGWdtcNYnP0uq9c4S4NibVG3Ni7VbKW7Q=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
"@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
@@ -366,6 +397,8 @@
"@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -400,7 +433,7 @@
"cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="],
"daisyui": ["daisyui@5.7.9", "", {}, "sha512-oPL7yddYPQrMsDmYtxNqPGBYc+gqm14GTwu7cDxAaUq0bPg5ZcaeR/DVMgSwgo26InVn8AECbUBUCK0rMs96Kg=="],
"daisyui": ["daisyui@5.7.17", "", {}, "sha512-w5U5FdL9hH4/zh8eLXytWQt3c0DgHS1WDw+1yuQykT8nSIE9Vc5wkJVeDymD1JlRDLANDawvN4iIGVaxgV6Cjw=="],
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
@@ -414,6 +447,10 @@
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
"discord-api-types": ["discord-api-types@0.38.53", "", {}, "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A=="],
"discord.js": ["discord.js@14.27.0", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.2", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.5", "discord-api-types": "^0.38.49", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="],
@@ -440,6 +477,8 @@
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
@@ -470,7 +509,7 @@
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"jose": ["jose@6.2.5", "", {}, "sha512-2E5L2yRp03FnwreJLJX8/r7mHiZICCf8kG7fAsTWkSQTDAcc46NIZoQLKy+EJ8sPoJlxyS4OQR5H70LjIZZlIQ=="],
"jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="],
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
@@ -482,7 +521,7 @@
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"kysely": ["kysely@0.29.4", "", {}, "sha512-y5mVgQNkMbs1eK9Xyc0pmNdabN2wHhRYY/5r4W5HrUT1rYCEPeVNSj1RUJeSDKT3U0p+mXCvLgkrFuIafYI6BA=="],
"kysely": ["kysely@0.29.5", "", {}, "sha512-ooa+eSbBNPTo3MycPEuW5jdrxQdQwdtB3LC3h43FiXQbIry5tR0C5lDG7eealK0E4D7XjrnOP5DIUg/LyjRMYQ=="],
"kysely-codegen": ["kysely-codegen@0.20.0", "", { "dependencies": { "chalk": "4.1.2", "cosmiconfig": "^9.0.0", "diff": "^8.0.3", "dotenv": "^17.2.4", "dotenv-expand": "^12.0.3", "micromatch": "^4.0.8", "minimist": "^1.2.8", "pluralize": "^8.0.0", "zod": "^4.3.6" }, "peerDependencies": { "@libsql/kysely-libsql": ">=0.3.0 <0.5.0", "@tediousjs/connection-string": "^1.0.0", "better-sqlite3": ">=7.6.2 <13.0.0", "kysely": ">=0.27.0 <1.0.0", "kysely-bun-sqlite": ">=0.3.2 <1.0.0", "kysely-bun-worker": ">=1.2.0 <2.0.0", "mysql2": ">=2.3.3 <4.0.0", "pg": ">=8.8.0 <9.0.0", "tarn": ">=3.0.0 <4.0.0", "tedious": ">=18.0.0 <20.0.0" }, "optionalPeers": ["@libsql/kysely-libsql", "@tediousjs/connection-string", "better-sqlite3", "kysely-bun-sqlite", "kysely-bun-worker", "mysql2", "pg", "tarn", "tedious"], "bin": { "kysely-codegen": "dist/cli/bin.js" } }, "sha512-LSi2KBG7uDmNCZ+XurLSA9LH7XFyyoQ6xb5DLJSInPTSYLVApjOP2KwO8mSaREWTtoX+C2AG2GTlYR0DLjTbcA=="],
@@ -514,6 +553,12 @@
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
"magic-bytes.js": ["magic-bytes.js@1.13.1", "", {}, "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
@@ -552,7 +597,7 @@
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
"pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="],
"pg": ["pg@8.23.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.16.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg=="],
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
@@ -630,11 +675,11 @@
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"svelte": ["svelte@5.56.8", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg=="],
"svelte": ["svelte@5.56.9", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA=="],
"svelte-adapter-bun": ["svelte-adapter-bun@1.0.1", "", { "dependencies": { "rolldown": "^1.0.0-beta.38" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0", "typescript": "^5" } }, "sha512-tNOvfm8BGgG+rmEA7hkmqtq07v7zoo4skLQc+hIoQ79J+1fkEMpJEA2RzCIe3aPc8JdrsMJkv3mpiZPMsgahjA=="],
"svelte-check": ["svelte-check@4.7.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.1", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-IW9ot9YqAoyv8FvyN+eb4ZTe8zgcKZrJLNYU6dzSKkGwEBsSPc4K7lmQ8bKn8W2YMXM6WDfZSSVOaGtekyUfOQ=="],
"svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
@@ -650,12 +695,16 @@
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
@@ -664,6 +713,8 @@
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
@@ -672,6 +723,10 @@
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
"@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
"@elysia/jwt/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
"@sveltejs/kit/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
@@ -692,6 +747,8 @@
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"pg/pg-protocol": ["pg-protocol@1.16.0", "", {}, "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg=="],
"thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="],
}
}
Executable
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
PG_APP="blade-and-brawn-db"
API_APP="blade-and-brawn-api"
LOCAL_PORT=5433
proxy_pid=""
cleanup() {
if [[ -n "$proxy_pid" ]]; then
kill "$proxy_pid" 2>/dev/null || true
fi
}
trap cleanup EXIT
echo "Fetching production DATABASE_URL from $API_APP..." >&2
prod_url=$(fly ssh console -a "$API_APP" -C "printenv DATABASE_URL" 2>/dev/null | grep -m1 '^postgres')
if [[ -z "$prod_url" ]]; then
echo "Failed to fetch DATABASE_URL from $API_APP" >&2
exit 1
fi
tunneled_url=$(echo "$prod_url" | sed -E "s#@[^/]+/#@localhost:${LOCAL_PORT}/#")
echo "Starting tunnel to $PG_APP on localhost:${LOCAL_PORT}..." >&2
fly proxy "${LOCAL_PORT}:5432" -a "$PG_APP" &
proxy_pid=$!
echo "Waiting for tunnel..." >&2
for _ in $(seq 1 30); do
nc -z localhost "$LOCAL_PORT" 2>/dev/null && break
sleep 0.5
done
target="${1:-migrate}"
case "$target" in
migrate)
DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:migrate:latest
;;
seed)
DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:seed
;;
both)
DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:migrate:latest
DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:seed
;;
*)
echo "Usage: $0 [migrate|seed|both]" >&2
exit 1
;;
esac
+1
View File
@@ -1,5 +1,6 @@
{
"name": "blade-and-brawn",
"version": "1.1.0",
"private": true,
"workspaces": [
"apps/*",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@blade-and-brawn/calculator",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"dependencies": {
"ml-levenberg-marquardt": "^5.0.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@blade-and-brawn/commerce",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@blade-and-brawn/domain",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@blade-and-brawn/domain": "workspace:*"
+8 -4
View File
@@ -1,19 +1,24 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ES2015", "ESNext", "DOM"],
"lib": [
"ES2015",
"ESNext",
"DOM"
],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"types": [
"bun"
],
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
@@ -21,7 +26,6 @@
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"forceConsistentCasingInFileNames": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,