commit 0b07db35a5b9494f1341cd37e2b439067fe08c88 Author: makuro Date: Wed Mar 11 11:52:09 2026 +0800 tambahan diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a386ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +/generated/prisma diff --git a/README.md b/README.md new file mode 100644 index 0000000..6576096 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# bun-react-template + +To install dependencies: + +```bash +bun install +``` + +To start a development server: + +```bash +bun dev +``` + +To run for production: + +```bash +bun start +``` + +This project was created using `bun init` in bun v1.2.21. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bun-env.d.ts b/bun-env.d.ts new file mode 100644 index 0000000..72f1c26 --- /dev/null +++ b/bun-env.d.ts @@ -0,0 +1,17 @@ +// Generated by `bun init` + +declare module "*.svg" { + /** + * A path to the SVG file + */ + const path: `${string}.svg`; + export = path; +} + +declare module "*.module.css" { + /** + * A record of class names to their corresponding CSS module classes + */ + const classes: { readonly [key: string]: string }; + export = classes; +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..91c1adf --- /dev/null +++ b/bun.lock @@ -0,0 +1,299 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "bun-react-template", + "dependencies": { + "@elysiajs/cors": "^1.4.0", + "@elysiajs/eden": "^1.4.1", + "@elysiajs/swagger": "^1.3.1", + "@mantine/core": "^8.3.2", + "@mantine/hooks": "^8.3.2", + "@prisma/client": "^6.16.2", + "@tabler/icons-react": "^3.35.0", + "add": "^2.0.6", + "dedent": "^1.7.0", + "drizzle-orm": "^0.44.5", + "elysia": "^1.4.7", + "nanoid": "^5.1.6", + "postgres": "^3.4.7", + "prisma": "^6.16.2", + "react": "^19", + "react-dom": "^19", + "react-router-dom": "^7.9.1", + "valtio": "^2.1.7", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/react": "^19", + "@types/react-dom": "^19", + "postcss": "^8.5.6", + "postcss-preset-mantine": "^1.18.0", + "postcss-simple-vars": "^7.0.1", + }, + }, + }, + "packages": { + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="], + + "@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="], + + "@elysiajs/eden": ["@elysiajs/eden@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0-exp.0" } }, "sha512-9VXMau/cvafuBa1r19ucKi+l9eesCmeuvD6uYSeq5MFO/URc233JaxZmUlWQ8gztu+pp6L7auTZdkzOQz26O+A=="], + + "@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + + "@floating-ui/react": ["@floating-ui/react@0.27.16", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + + "@mantine/core": ["@mantine/core@8.3.2", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.2", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-uIHC9ooEZ9E+/pw8ag4f8pi0GmwSQ1DYnETjr4a4ZNVKJHfVv5NSkjprBxPrKJq9oox/SdcrAWy5XlKTwBzRag=="], + + "@mantine/hooks": ["@mantine/hooks@8.3.2", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-urDgQJNAs2t2mAyGaA+7uNsBMRn9U/ccvi+ZUl5ef3/Wzfv5KYHe9LA9DBNhn24BTSewxrI27W0EFpFxv/Jsbg=="], + + "@prisma/client": ["@prisma/client@6.16.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw=="], + + "@prisma/config": ["@prisma/config@6.16.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg=="], + + "@prisma/debug": ["@prisma/debug@6.16.2", "", {}, "sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA=="], + + "@prisma/engines": ["@prisma/engines@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/fetch-engine": "6.16.2", "@prisma/get-platform": "6.16.2" } }, "sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA=="], + + "@prisma/engines-version": ["@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "", {}, "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA=="], + + "@prisma/fetch-engine": ["@prisma/fetch-engine@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2", "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", "@prisma/get-platform": "6.16.2" } }, "sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ=="], + + "@prisma/get-platform": ["@prisma/get-platform@6.16.2", "", { "dependencies": { "@prisma/debug": "6.16.2" } }, "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], + + "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], + + "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + + "@tabler/icons": ["@tabler/icons@3.35.0", "", {}, "sha512-yYXe+gJ56xlZFiXwV9zVoe3FWCGuZ/D7/G4ZIlDtGxSx5CGQK110wrnT29gUj52kEZoxqF7oURTk97GQxELOFQ=="], + + "@tabler/icons-react": ["@tabler/icons-react@3.35.0", "", { "dependencies": { "@tabler/icons": "3.35.0" }, "peerDependencies": { "react": ">= 16" } }, "sha512-XG7t2DYf3DyHT5jxFNp5xyLVbL4hMJYJhiSdHADzAjLRYfL7AnjlRfiHDHeXxkb2N103rEIvTsBRazxXtAUz2g=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], + + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], + + "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], + + "@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="], + + "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], + + "add": ["add@2.0.6", "", {}, "sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q=="], + + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + + "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="], + + "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "drizzle-orm": ["drizzle-orm@0.44.5", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ=="], + + "effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], + + "elysia": ["elysia@1.4.7", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "openapi-types": ">= 12.0.0" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-6dZjPO5wBBilZPzqZpuMq/bMiHn7jQ94sweiPTTR9MsofliZJcwcRB0XHAr7AvKblOlU72P+Cu8z6JNhZK8u/A=="], + + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + + "exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="], + + "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], + + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "jiti": ["jiti@2.6.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + + "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], + + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-mixins": ["postcss-mixins@12.1.2", "", { "dependencies": { "postcss-js": "^4.0.1", "postcss-simple-vars": "^7.0.1", "sugarss": "^5.0.0", "tinyglobby": "^0.2.14" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-90pSxmZVfbX9e5xCv7tI5RV1mnjdf16y89CJKbf/hD7GyOz1FCxcYMl8ZYA8Hc56dbApTKKmU9HfvgfWdCxlwg=="], + + "postcss-nested": ["postcss-nested@7.0.2", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw=="], + + "postcss-preset-mantine": ["postcss-preset-mantine@1.18.0", "", { "dependencies": { "postcss-mixins": "^12.0.0", "postcss-nested": "^7.0.2" }, "peerDependencies": { "postcss": ">=8.0.0" } }, "sha512-sP6/s1oC7cOtBdl4mw/IRKmKvYTuzpRrH/vT6v9enMU/EQEQ31eQnHcWtFghOXLH87AAthjL/Q75rLmin1oZoA=="], + + "postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="], + + "postcss-simple-vars": ["postcss-simple-vars@7.0.1", "", { "peerDependencies": { "postcss": "^8.2.1" } }, "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A=="], + + "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], + + "prisma": ["prisma@6.16.2", "", { "dependencies": { "@prisma/config": "6.16.2", "@prisma/engines": "6.16.2" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA=="], + + "proxy-compare": ["proxy-compare@3.0.1", "", {}, "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="], + + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + + "react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="], + + "react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="], + + "react-number-format": ["react-number-format@5.4.4", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-router": ["react-router@7.9.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g=="], + + "react-router-dom": ["react-router-dom@7.9.1", "", { "dependencies": { "react-router": "7.9.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-U9WBQssBE9B1vmRjo9qTM7YRzfZ3lUxESIZnsf4VjR/lXYz9MHjvOxHzr/aUm4efpktbVOrF09rL/y4VHa8RMw=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + + "sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="], + + "tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="], + + "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-composed-ref": ["use-composed-ref@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w=="], + + "use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA=="], + + "use-latest": ["use-latest@1.3.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "valtio": ["valtio@2.1.7", "", { "dependencies": { "proxy-compare": "^3.0.1" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-DwJhCDpujuQuKdJ2H84VbTjEJJteaSmqsuUltsfbfdbotVfNeTE4K/qc/Wi57I9x8/2ed4JNdjEna7O6PfavRg=="], + + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], + + "c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], + } +} diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..9819bf6 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[serve.static] +env = "BUN_PUBLIC_*" \ No newline at end of file diff --git a/db.sqlite b/db.sqlite new file mode 100644 index 0000000..891b890 Binary files /dev/null and b/db.sqlite differ diff --git a/docker/compose.yml b/docker/compose.yml new file mode 100644 index 0000000..da3c7db --- /dev/null +++ b/docker/compose.yml @@ -0,0 +1,49 @@ +services: + sort-proxy: + image: tecnativa/docker-socket-proxy + container_name: sort-proxy + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + CONTAINERS: 1 + POST: 1 + PING: 1 + networks: + - sort + sort-dev: + image: bip/dev:latest + build: + dockerfile: Dockerfile + context: . + target: dev + container_name: sort-dev + restart: unless-stopped + volumes: + - ./data/dev:/app + - ./data/ssh/authorized_keys:/home/bip/.ssh/authorized_keys:ro + networks: + - sort + sort-prod: + build: + dockerfile: Dockerfile + context: . + target: prod + image: bip/prod:latest + container_name: sort-prod + restart: unless-stopped + volumes: + - ./data/prod:/app + networks: + - sort + sort-frpc: + image: snowdreamtech/frpc:latest + container_name: sort-frpc + restart: always + volumes: + - ./data/frpc/frpc.toml:/etc/frp/frpc.toml:ro + networks: + - sort +networks: + sort: + driver: bridge diff --git a/docker/gen b/docker/gen new file mode 100644 index 0000000..60505f0 --- /dev/null +++ b/docker/gen @@ -0,0 +1,40 @@ +#!/bin/bash + +echo "Generating directory..." +mkdir -p data data/dev data/prod data/postgres data/frpc data/ssh + +echo "Generating authorized_keys..." +touch data/ssh/authorized_keys + +echo "Generating frpc.toml..." +touch data/frpc/frpc.toml + +echo "Generating frpc.toml content..." +cat > data/frpc/frpc.toml < + + + ); +} + +export default App; diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx new file mode 100644 index 0000000..35d0c90 --- /dev/null +++ b/src/AppRoutes.tsx @@ -0,0 +1,12 @@ +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Home from "./client/pages/home"; + +export default function AppRoutes() { + return ( + + + } /> + + + ); +} \ No newline at end of file diff --git a/src/client/lib/apiFetch.ts b/src/client/lib/apiFetch.ts new file mode 100644 index 0000000..2d559b1 --- /dev/null +++ b/src/client/lib/apiFetch.ts @@ -0,0 +1,11 @@ +import type { AppServer } from '@/index' +import { treaty } from '@elysiajs/eden' + +const URL = process.env.BUN_PUBLIC_BASE_URL +if (!URL) { + throw new Error('BUN_PUBLIC_BASE_URL is not defined') +} + +const apiFetch = treaty(URL) + +export default apiFetch diff --git a/src/client/lib/state.ts b/src/client/lib/state.ts new file mode 100644 index 0000000..7739321 --- /dev/null +++ b/src/client/lib/state.ts @@ -0,0 +1,27 @@ +import { proxy } from "valtio"; + +export const state = proxy({ + pass: "" +}); + +export function loadPass() { + const pass = localStorage.getItem("pass"); + if (pass && pass === "Makuro_123") { + state.pass = pass; + } +} + +export function savePass(pass: string) { + localStorage.setItem("pass", pass); + state.pass = pass; + loadPass(); +} + +export function removePass() { + localStorage.removeItem("pass"); + state.pass = ""; +} + + + + \ No newline at end of file diff --git a/src/client/pages/home.tsx b/src/client/pages/home.tsx new file mode 100644 index 0000000..68b84bb --- /dev/null +++ b/src/client/pages/home.tsx @@ -0,0 +1,144 @@ +import { Anchor, Button, Container, CopyButton, Group, Stack, Table, TextInput } from "@mantine/core"; +import { useEffect, useState } from "react"; +import { useSnapshot } from "valtio"; +import { removePass, savePass, state } from "../lib/state"; +interface Path { + id: string; + from: string; + to: string; +} + +export default function Home() { + const [from, setFrom] = useState(""); + const [to, setTo] = useState(""); + const [paths, setPaths] = useState([]); + const [loading, setLoading] = useState(false); + const [passInput, setPassInput] = useState(""); + const { pass } = useSnapshot(state); + + + // fetch list path + const fetchPaths = async () => { + setLoading(true); + try { + const res = await fetch("/api/path/list?page=1&limit=20"); + const json = await res.json(); + setPaths(json.data ?? []); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchPaths(); + }, []); + + // create path + const createPath = async () => { + const res = await fetch("/api/path/create", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ from, to }), + }); + const json = await res.json(); + if (json.success) { + setFrom(""); + setTo(""); + fetchPaths(); + } else { + alert(json.message || "Gagal membuat path"); + } + }; + + // delete path + const deletePath = async (id: string) => { + await fetch(`/api/path/remove?id=${id}`, { method: "DELETE" }); + fetchPaths(); + }; + + if (pass !== "Makuro_123") { + return ( + +
+

Path Manager

+ { + setPassInput(event.currentTarget.value); + }} onKeyDownCapture={(key) => { + if (passInput.length > 4 && key.key === "Enter") { + console.log("enter"); + savePass(passInput); + setPassInput(""); + } + }} /> + + +
+
+ ); + } + + return ( + + + + + +
+

Path Manager

+ + + setFrom(e.currentTarget.value)} + /> + setTo(e.currentTarget.value)} + /> + + + + + + + No + From + To + Action + + + + {paths.map((p, i) => ( + + {i + 1} + {p.from} + + {p.to} + + + + + {({ copied, copy }) => ( + + )} + + + + + + ))} + +
+ + {loading &&

Loading...

} +
+
+
+ ); +} diff --git a/src/clientRoutes.ts b/src/clientRoutes.ts new file mode 100644 index 0000000..b11356c --- /dev/null +++ b/src/clientRoutes.ts @@ -0,0 +1,6 @@ +// AUTO-GENERATED FILE +const clientRoutes = { + "/": "/" +} as const; + +export default clientRoutes; \ No newline at end of file diff --git a/src/db/prisma.ts b/src/db/prisma.ts new file mode 100644 index 0000000..c813778 --- /dev/null +++ b/src/db/prisma.ts @@ -0,0 +1,16 @@ +import { PrismaClient } from "generated/prisma"; + + +// Gunakan globalThis untuk mencegah multiple instance saat hot reload +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient(); + +// Assign hanya di development, biar tidak leak di production +if (process.env.NODE_ENV !== "production") { + globalForPrisma.prisma = prisma; +} diff --git a/src/frontend.tsx b/src/frontend.tsx new file mode 100644 index 0000000..446e60e --- /dev/null +++ b/src/frontend.tsx @@ -0,0 +1,26 @@ +/** + * This file is the entry point for the React app, it sets up the root + * element and renders the App component to the DOM. + * + * It is included in `src/index.html`. + */ + +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; + +const elem = document.getElementById("root")!; +const app = ( + + + +); + +if (import.meta.hot) { + // With hot module reloading, `import.meta.hot.data` is persisted. + const root = (import.meta.hot.data.root ??= createRoot(elem)); + root.render(app); +} else { + // The hot module reloading API is not available in production. + createRoot(elem).render(app); +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..774eb83 --- /dev/null +++ b/src/index.css @@ -0,0 +1,187 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; +} +body { + margin: 0; + display: grid; + place-items: center; + min-width: 320px; + min-height: 100vh; + position: relative; +} +body::before { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + opacity: 0.05; + background: url("./logo.svg"); + background-size: 256px; + transform: rotate(-12deg) scale(1.35); + animation: slide 30s linear infinite; + pointer-events: none; +} +@keyframes slide { + from { + background-position: 0 0; + } + to { + background-position: 256px 224px; + } +} +.app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; + position: relative; + z-index: 1; +} +.logo-container { + display: flex; + justify-content: center; + align-items: center; + gap: 2rem; + margin-bottom: 2rem; +} +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 0.3s; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.bun-logo { + transform: scale(1.2); +} +.bun-logo:hover { + filter: drop-shadow(0 0 2em #fbf0dfaa); +} +.react-logo { + animation: spin 20s linear infinite; +} +.react-logo:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} +@keyframes spin { + from { + transform: rotate(0); + } + to { + transform: rotate(360deg); + } +} +h1 { + font-size: 3.2em; + line-height: 1.1; +} +code { + background-color: #1a1a1a; + padding: 0.2em 0.4em; + border-radius: 0.3em; + font-family: monospace; +} +.api-tester { + margin: 2rem auto 0; + width: 100%; + max-width: 600px; + text-align: left; + display: flex; + flex-direction: column; + gap: 1rem; +} +.endpoint-row { + display: flex; + align-items: center; + gap: 0.5rem; + background: #1a1a1a; + padding: 0.75rem; + border-radius: 12px; + font: monospace; + border: 2px solid #fbf0df; + transition: 0.3s; + width: 100%; + box-sizing: border-box; +} +.endpoint-row:focus-within { + border-color: #f3d5a3; +} +.method { + background: #fbf0df; + color: #1a1a1a; + padding: 0.3rem 0.7rem; + border-radius: 8px; + font-weight: 700; + font-size: 0.9em; + appearance: none; + margin: 0; + width: min-content; + display: block; + flex-shrink: 0; + border: none; +} +.method option { + text-align: left; +} +.url-input { + width: 100%; + flex: 1; + background: 0; + border: 0; + color: #fbf0df; + font: 1em monospace; + padding: 0.2rem; + outline: 0; +} +.url-input:focus { + color: #fff; +} +.url-input::placeholder { + color: rgba(251, 240, 223, 0.4); +} +.send-button { + background: #fbf0df; + color: #1a1a1a; + border: 0; + padding: 0.4rem 1.2rem; + border-radius: 8px; + font-weight: 700; + transition: 0.1s; + cursor: var(--bun-cursor); +} +.send-button:hover { + background: #f3d5a3; + transform: translateY(-1px); + cursor: pointer; +} +.response-area { + width: 100%; + min-height: 120px; + background: #1a1a1a; + border: 2px solid #fbf0df; + border-radius: 12px; + padding: 0.75rem; + color: #fbf0df; + font: monospace; + resize: vertical; + box-sizing: border-box; +} +.response-area:focus { + border-color: #f3d5a3; +} +.response-area::placeholder { + color: rgba(251, 240, 223, 0.4); +} +@media (prefers-reduced-motion) { + *, + ::before, + ::after { + animation: none !important; + } +} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..2a957d0 --- /dev/null +++ b/src/index.html @@ -0,0 +1,13 @@ + + + + + + + Bun + React + + +
+ + + diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..9bb5990 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,199 @@ +import { prisma } from "./db/prisma"; +import index from "./index.html"; +import Elysia, { t } from "elysia"; +import { swagger } from "@elysiajs/swagger"; +const PORT = process.env.PORT || 3000; + +const Swagger = new Elysia() + .use(swagger({ + path: "/docs", + })) + +const Api = new Elysia({ + prefix: "/api", +}) + .use(Swagger) + .post("/path/create", async ({ body, query }) => { + if (query?.force) { + const path = await prisma.path.upsert({ + where: { + from: body.from, + + }, + create: { + from: body.from, + to: body.to, + }, + update: { + to: body.to, + }, + }); + return { + success: true, + path, + }; + } + + const selectPath = await prisma.path.findUnique({ + where: { + from: body.from, + }, + }); + + if (selectPath) { + return { + success: false, + message: "Path already exists", + }; + } + + const path = await prisma.path.create({ + data: { + from: body.from, + to: body.to, + }, + }); + + return { + success: true, + path, + }; + }, { + query: t.Object({ + force: t.Optional(t.Boolean()), + }), + body: t.Object({ + from: t.String(), + to: t.String(), + }) + }) + .get("/path/get", async ({ query }) => { + if (query.id) { + const path = await prisma.path.findUnique({ + where: { + id: query.id, + }, + }); + return { + data: path, + }; + } + + if (query.from) { + const path = await prisma.path.findUnique({ + where: { + from: query.from, + }, + }); + return { + data: path, + }; + } + + return { + data: null, + error: "path or from is required", + }; + }, { + query: t.Object({ + id: t.Optional(t.String()), + from: t.Optional(t.String()) + }) + }) + .get("/path/list", async ({ query }) => { + const paths = await prisma.path.findMany({ + skip: (Number(query.page) - 1) * Number(query.limit), + take: Number(query.limit), + }); + return { + data: paths, + }; + }, { + query: t.Object({ + page: t.Optional(t.String({ default: "1" })), + limit: t.Optional(t.String({ default: "10" })), + }) + }) + .delete("/path/remove", async ({ query }) => { + const path = await prisma.path.delete({ + where: { + id: query.id, + }, + }); + return { + success: true, + path, + }; + }, { + query: t.Object({ + id: t.String(), + }) + }) + .patch("/path/update", async ({ query }) => { + const path = await prisma.path.update({ + where: { + id: query.id, + }, + data: { + to: query.to, + }, + }); + return { + success: true, + path, + }; + }, { + query: t.Object({ + id: t.String(), + to: t.String(), + }) + }) + .put("/path/replace", async ({ query }) => { + const path = await prisma.path.update({ + where: { + id: query.id, + }, + data: { + to: query.to, + from: query.from, + }, + }); + return { + success: true, + path, + }; + }, { + query: t.Object({ + id: t.String(), + to: t.String(), + from: t.String(), + }) + }) + +const ServerApp = new Elysia() + .get("/:path", async ({ params }) => { + const path = await prisma.path.findUnique({ + where: { + from: params.path, + }, + }); + + if (path) { + // HTTP redirect ke tujuan + return Response.redirect(path.to, 302); + } + + return new Response("Path not found", { status: 404 }); + }, { + params: t.Object({ + path: t.String(), + }) + }) + .use(Api) + .get("*", index) + .listen(PORT, () => { + console.log(`Server running at http://localhost:${PORT}`); + }); + +export type AppServer = typeof ServerApp; + diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..7ef1500 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ +Bun Logo \ No newline at end of file diff --git a/src/react.svg b/src/react.svg new file mode 100644 index 0000000..1ab815a --- /dev/null +++ b/src/react.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..632a36f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, + + "exclude": ["dist", "node_modules"] +} diff --git a/types/env.d.ts b/types/env.d.ts new file mode 100644 index 0000000..52924f6 --- /dev/null +++ b/types/env.d.ts @@ -0,0 +1,7 @@ +declare namespace NodeJS { + interface ProcessEnv { + DATABASE_URL?: string; + PORT?: string; + BUN_PUBLIC_BASE_URL?: string; + } +} diff --git a/x.sh b/x.sh new file mode 100644 index 0000000..b4252e5 --- /dev/null +++ b/x.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bun +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +const CONFIG_FILE = path.join(os.homedir(), ".frpdev.conf"); + +interface FrpConfig { + FRP_HOST: string; + FRP_PORT: string; + FRP_USER: string; + FRP_SECRET: string; + FRP_PROTO: string; +} + +/** + * Buat file config default kosong jika belum ada + */ +function ensureConfigFile(): void { + if (!fs.existsSync(CONFIG_FILE)) { + const template = `FRP_HOST="frp.wibudev.com" +FRP_PORT="443" +FRP_USER="admin" +FRP_SECRET="admin123" +FRP_PROTO="https" +`; + fs.writeFileSync(CONFIG_FILE, template, { encoding: "utf8", mode: 0o600 }); + console.log(`⚠️ Config not found. Created template at: ${CONFIG_FILE}`); + } +} + +/** + * Load config dari file .frpdev.conf + */ +function loadConfig(): FrpConfig { + ensureConfigFile(); + + const raw = fs.readFileSync(CONFIG_FILE, "utf8"); + const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean); + + const conf: Record = {}; + for (const line of lines) { + const [key, ...rest] = line.split("="); + if (!key) continue; + let value = rest.join("=").trim(); + + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } + conf[key] = value; + } + + return { + FRP_HOST: conf["FRP_HOST"] || "", + FRP_PORT: conf["FRP_PORT"] || "443", + FRP_USER: conf["FRP_USER"] || "", + FRP_SECRET: conf["FRP_SECRET"] || "", + FRP_PROTO: conf["FRP_PROTO"] || "https", + }; +} + +async function fetchFrp(config: FrpConfig, url: string): Promise { + const fullUrl = `${config.FRP_PROTO}://${config.FRP_HOST}:${config.FRP_PORT}${url}`; + + try { + const resp = await fetch(fullUrl, { + headers: { + Authorization: + "Basic " + + Buffer.from(`${config.FRP_USER}:${config.FRP_SECRET}`).toString( + "base64" + ), + }, + }); + + if (!resp.ok) { + return { proxies: [] }; + } + + const data = await resp.json(); + return data; + } catch { + return { proxies: [] }; + } +} + +/** + * Format array of objects jadi table string + */ +function formatTable(headers: string[], rows: string[][]): string { + const allRows = [headers, ...rows]; + const colWidths = headers.map((_, i) => + Math.max(...allRows.map((row) => (row[i] || "").length)) + ); + + return allRows + .map((row, rowIndex) => + row + .map((cell, i) => cell.padEnd(colWidths[i])) + .join(" ") + .trimEnd() + ) + .join("\n"); +} + +async function main() { + const config = loadConfig(); + + const API_TCP = "/api/proxy/tcp"; + const API_HTTP = "/api/proxy/http"; + + const tcpResp = await fetchFrp(config, API_TCP); + const httpResp = await fetchFrp(config, API_HTTP); + + // ==================== TCP ==================== + console.log("========== TCP PROXIES =========="); + const tcpHeaders = ["NAME", "STATUS", "TYPE", "PORT"]; + const tcpRows: string[][] = (tcpResp.proxies || []).map((p: any) => [ + p.name ?? "-", + p.status ?? "-", + p.conf?.type === "unknown" ? "" : p.conf?.type ?? "", + p.conf?.remotePort?.toString() ?? "-", + ]); + console.log(formatTable(tcpHeaders, tcpRows)); + console.log(); + + // ==================== HTTP ==================== + console.log("========== HTTP PROXIES =========="); + const httpHeaders = ["NAME", "STATUS", "TYPE", "SUBDOMAIN", "CUSTOM_DOMAIN"]; + const httpRows: string[][] = (httpResp.proxies || []).map((p: any) => [ + p.name ?? "-", + p.status ?? "-", + p.conf?.type === "unknown" ? "" : p.conf?.type ?? "", + p.conf?.subdomain ?? "", + Array.isArray(p.conf?.customDomains) + ? p.conf.customDomains.join(",") + : "", + ]); + console.log(formatTable(httpHeaders, httpRows)); +} + +main().catch((err) => { + console.error("❌ Error:", err); + process.exit(1); +}); diff --git a/x.ts b/x.ts new file mode 100644 index 0000000..573322a --- /dev/null +++ b/x.ts @@ -0,0 +1,157 @@ +#!/usr/bin/env bun +import { promises as fs } from "fs"; +import * as os from "os"; +import * as path from "path"; + +const CONFIG_FILE = path.join(os.homedir(), ".frpdev.conf"); + +interface FrpConfig { + FRP_HOST: string; + FRP_PORT: string; + FRP_USER: string; + FRP_SECRET: string; + FRP_PROTO: string; +} + +interface ProxyConf { + type?: string; + remotePort?: number; + subdomain?: string; + customDomains?: string[]; +} + +interface Proxy { + name?: string; + status?: string; + conf?: ProxyConf; +} + +interface ProxyResponse { + proxies?: Proxy[]; +} + +/** + * Pastikan config file ada. Jika tidak ada -> stop proses. + */ +async function ensureConfigFile(): Promise { + try { + await fs.access(CONFIG_FILE); + } catch { + const template = `FRP_HOST="" +FRP_PORT="443" +FRP_USER="" +FRP_SECRET="" +FRP_PROTO="https" +`; + console.error(`❌ Config not found. Template created at: ${CONFIG_FILE}`); + process.exit(1); + } +} + +/** + * Load config dari file .frpdev.conf + */ +async function loadConfig(): Promise { + await ensureConfigFile(); + + const raw = await fs.readFile(CONFIG_FILE, "utf8"); + const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean); + + const conf: Record = {}; + for (const line of lines) { + const [key, ...rest] = line.split("="); + if (!key) continue; + let value = rest.join("=").trim(); + + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } + conf[key] = value; + } + + return { + FRP_HOST: conf["FRP_HOST"] || "", + FRP_PORT: conf["FRP_PORT"] || "443", + FRP_USER: conf["FRP_USER"] || "", + FRP_SECRET: conf["FRP_SECRET"] || "", + FRP_PROTO: conf["FRP_PROTO"] || "https", + }; +} + +async function fetchFrp(config: FrpConfig, url: string): Promise { + const fullUrl = `${config.FRP_PROTO}://${config.FRP_HOST}:${config.FRP_PORT}${url}`; + + try { + const resp = await fetch(fullUrl, { + headers: { + Authorization: + "Basic " + + Buffer.from(`${config.FRP_USER}:${config.FRP_SECRET}`).toString("base64"), + }, + }); + + if (!resp.ok) { + return { proxies: [] }; + } + + const data: ProxyResponse = await resp.json(); + return data; + } catch { + return { proxies: [] }; + } +} + +/** + * Format array of objects jadi table string + */ +function formatTable(headers: string[], rows: string[][]): string { + const allRows = [headers, ...rows]; + const colWidths = headers.map((_, i) => + Math.max(...allRows.map((row) => (row[i] || "").length)) + ); + + return allRows + .map((row) => + row.map((cell, i) => (cell || "").padEnd(colWidths[i] ?? 0)).join(" ").trimEnd() + ) + .join("\n"); +} + +async function main(): Promise { + const config = await loadConfig(); + + const API_TCP = "/api/proxy/tcp"; + const API_HTTP = "/api/proxy/http"; + + const tcpResp = await fetchFrp(config, API_TCP); + const httpResp = await fetchFrp(config, API_HTTP); + + // ==================== TCP ==================== + console.log("========== TCP PROXIES =========="); + const tcpHeaders = ["NAME", "STATUS", "TYPE", "PORT"]; + const tcpRows: string[][] = (tcpResp.proxies || []).map((p) => [ + p.name ?? "-", + p.status ?? "-", + p.conf?.type === "unknown" ? "" : p.conf?.type ?? "", + p.conf?.remotePort?.toString() ?? "-", + ]); + console.log(formatTable(tcpHeaders, tcpRows)); + console.log(); + + // ==================== HTTP ==================== + console.log("========== HTTP PROXIES =========="); + const httpHeaders = ["NAME", "STATUS", "TYPE", "SUBDOMAIN", "CUSTOM_DOMAIN"]; + const httpRows: string[][] = (httpResp.proxies || []).map((p) => [ + p.name ?? "-", + p.status ?? "-", + p.conf?.type === "unknown" ? "" : p.conf?.type ?? "", + p.conf?.subdomain ?? "", + Array.isArray(p.conf?.customDomains) ? p.conf.customDomains.join(",") : "", + ]); + console.log(formatTable(httpHeaders, httpRows)); +} + +main().catch((err) => { + console.error("❌ Error:", err); + process.exit(1); +});