some better auth stuff
This commit is contained in:
5
app/.gitignore
vendored
Normal file
5
app/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
# Keep environment variables out of version control
|
||||||
|
.env
|
||||||
|
|
||||||
|
generated/prisma
|
8
app/prisma/migrations/20250909175946_init/migration.sql
Normal file
8
app/prisma/migrations/20250909175946_init/migration.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "public"."Test" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Test_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `Test` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "public"."Test";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "public"."user" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"image" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "public"."session" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"ipAddress" TEXT,
|
||||||
|
"userAgent" TEXT,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "public"."account" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"accountId" TEXT NOT NULL,
|
||||||
|
"providerId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"accessToken" TEXT,
|
||||||
|
"refreshToken" TEXT,
|
||||||
|
"idToken" TEXT,
|
||||||
|
"accessTokenExpiresAt" TIMESTAMP(3),
|
||||||
|
"refreshTokenExpiresAt" TIMESTAMP(3),
|
||||||
|
"scope" TEXT,
|
||||||
|
"password" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "account_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "public"."verification" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"identifier" TEXT NOT NULL,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "user_email_key" ON "public"."user"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "session_token_key" ON "public"."session"("token");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
3
app/prisma/migrations/migration_lock.toml
Normal file
3
app/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "postgresql"
|
74
app/prisma/schema.prisma
Normal file
74
app/prisma/schema.prisma
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||||
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id
|
||||||
|
name String
|
||||||
|
email String
|
||||||
|
emailVerified Boolean @default(false)
|
||||||
|
image String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
sessions Session[]
|
||||||
|
accounts Account[]
|
||||||
|
|
||||||
|
@@unique([email])
|
||||||
|
@@map("user")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id
|
||||||
|
expiresAt DateTime
|
||||||
|
token String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
ipAddress String?
|
||||||
|
userAgent String?
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([token])
|
||||||
|
@@map("session")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Account {
|
||||||
|
id String @id
|
||||||
|
accountId String
|
||||||
|
providerId String
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
accessToken String?
|
||||||
|
refreshToken String?
|
||||||
|
idToken String?
|
||||||
|
accessTokenExpiresAt DateTime?
|
||||||
|
refreshTokenExpiresAt DateTime?
|
||||||
|
scope String?
|
||||||
|
password String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("account")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Verification {
|
||||||
|
id String @id
|
||||||
|
identifier String
|
||||||
|
value String
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
|
@@map("verification")
|
||||||
|
}
|
17
app/src/lib/api/auth.ts
Normal file
17
app/src/lib/api/auth.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { betterAuth } from 'better-auth';
|
||||||
|
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: prismaAdapter(prisma, {
|
||||||
|
provider: 'postgresql'
|
||||||
|
}),
|
||||||
|
socialProviders: {
|
||||||
|
microsoft: {
|
||||||
|
clientId: process.env.MICROSOFT_CLIENT_ID as string,
|
||||||
|
clientSecret: process.env.MICROSOFT_CLIENT_SECRET as string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
19
app/src/lib/api/macro.ts
Normal file
19
app/src/lib/api/macro.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Elysia from 'elysia';
|
||||||
|
import { auth } from './auth';
|
||||||
|
|
||||||
|
export const betterAuth = new Elysia({ name: 'better-auth' }).mount(auth.handler).macro({
|
||||||
|
auth: {
|
||||||
|
async resolve({ status, request: { headers } }) {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) return status(401);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: session.user,
|
||||||
|
session: session.session
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
0
app/src/lib/api/routes/docker/index.ts
Normal file
0
app/src/lib/api/routes/docker/index.ts
Normal file
346
app/src/lib/api/utils/projectmanager.ts
Normal file
346
app/src/lib/api/utils/projectmanager.ts
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
import { spawn, $, write, file } from 'bun';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
declare namespace Bun {
|
||||||
|
export const YAML: {
|
||||||
|
parse(text: string): any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MinecraftEnvironment {
|
||||||
|
EULA: string;
|
||||||
|
VERSION?: string;
|
||||||
|
TYPE?: 'VANILLA' | 'FORGE' | 'FABRIC' | 'PAPER' | 'SPIGOT' | 'BUKKIT' | 'PURPUR' | 'MODDED';
|
||||||
|
DIFFICULTY?: 'PEACEFUL' | 'EASY' | 'NORMAL' | 'HARD';
|
||||||
|
MODE?: 'SURVIVAL' | 'CREATIVE' | 'ADVENTURE' | 'SPECTATOR';
|
||||||
|
MAX_PLAYERS?: string;
|
||||||
|
MOTD?: string;
|
||||||
|
LEVEL_NAME?: string;
|
||||||
|
SEED?: string;
|
||||||
|
PVP?: string;
|
||||||
|
ENABLE_COMMAND_BLOCK?: string;
|
||||||
|
SPAWN_PROTECTION?: string;
|
||||||
|
MAX_WORLD_SIZE?: string;
|
||||||
|
VIEW_DISTANCE?: string;
|
||||||
|
ONLINE_MODE?: string;
|
||||||
|
ALLOW_NETHER?: string;
|
||||||
|
ANNOUNCE_PLAYER_ACHIEVEMENTS?: string;
|
||||||
|
MEMORY?: string;
|
||||||
|
JVM_OPTS?: string;
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MinecraftService {
|
||||||
|
image: 'itzg/minecraft-server';
|
||||||
|
container_name: string;
|
||||||
|
tty: true;
|
||||||
|
stdin_open: true;
|
||||||
|
ports: string[];
|
||||||
|
environment: MinecraftEnvironment;
|
||||||
|
volumes: ['./data:/data'];
|
||||||
|
restart?: 'unless-stopped' | 'no';
|
||||||
|
networks?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple YAML stringifier for basic objects
|
||||||
|
function stringifyYAML(obj: any, indent = 0): string {
|
||||||
|
const spaces = ' '.repeat(indent);
|
||||||
|
|
||||||
|
if (obj === null || obj === undefined) {
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
// handle strings that need quoting
|
||||||
|
if (obj.includes(':') || obj.includes('\n') || obj.includes('#')) {
|
||||||
|
return `"${obj.replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'number' || typeof obj === 'boolean') {
|
||||||
|
return String(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
if (obj.length === 0) return '[]';
|
||||||
|
return obj.map((item) => `${spaces}- ${stringifyYAML(item, indent + 2)}`).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const entries = Object.entries(obj);
|
||||||
|
if (entries.length === 0) return '{}';
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.map(([key, value]) => {
|
||||||
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||||
|
return `${spaces}${key}:\n${stringifyYAML(value, indent + 2)}`;
|
||||||
|
} else {
|
||||||
|
return `${spaces}${key}: ${stringifyYAML(value, 0)}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// minimal Minecraft docker-compose template
|
||||||
|
const baseTemplate: { services: { mc: MinecraftService } } = {
|
||||||
|
services: {
|
||||||
|
mc: {
|
||||||
|
image: 'itzg/minecraft-server',
|
||||||
|
container_name: 'minecraft',
|
||||||
|
tty: true,
|
||||||
|
stdin_open: true,
|
||||||
|
ports: ['25565:25565'],
|
||||||
|
environment: {
|
||||||
|
EULA: 'TRUE',
|
||||||
|
TYPE: 'PAPER',
|
||||||
|
ENABLE_WHITELIST: 'true',
|
||||||
|
WHITELIST: 'f396e2b9-cbb1-46a0-bb72-96898a1ca44d',
|
||||||
|
DIFFICULTY: 'NORMAL',
|
||||||
|
SPAWN_PROTECTION: '0',
|
||||||
|
RCON_CMDS_FIRST_CONNECT: `op vaporvee`
|
||||||
|
},
|
||||||
|
volumes: ['./data:/data']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const forbiddenKeys = new Set([
|
||||||
|
'ENABLE_QUERY',
|
||||||
|
'QUERY_PORT',
|
||||||
|
'ENABLE_RCON',
|
||||||
|
'RCON_PORT',
|
||||||
|
'RCON_PASSWORD'
|
||||||
|
]);
|
||||||
|
|
||||||
|
class ProjectManager {
|
||||||
|
constructor(private baseDir = join(process.cwd(), 'docker-projects')) {}
|
||||||
|
|
||||||
|
projectPath(name: string) {
|
||||||
|
return join(this.baseDir, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
composePath(name: string) {
|
||||||
|
return join(this.projectPath(name), 'docker-compose.yml');
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProject(name: string) {
|
||||||
|
await $`mkdir -p ${this.projectPath(name)}`;
|
||||||
|
await write(this.composePath(name), stringifyYAML(baseTemplate));
|
||||||
|
}
|
||||||
|
|
||||||
|
async patchProject(name: string, patch: any) {
|
||||||
|
const path = this.composePath(name);
|
||||||
|
const f = file(path);
|
||||||
|
if (!(await f.exists())) throw new Error('Project not found');
|
||||||
|
const text = await f.text();
|
||||||
|
const config = Bun.YAML.parse(text);
|
||||||
|
|
||||||
|
if (patch.services?.mc) {
|
||||||
|
for (const [key, val] of Object.entries(patch.services.mc)) {
|
||||||
|
if (!forbiddenKeys.has(key)) {
|
||||||
|
if (typeof val === 'object' && !Array.isArray(val)) {
|
||||||
|
config.services.mc[key] = { ...config.services.mc[key], ...val };
|
||||||
|
} else {
|
||||||
|
config.services.mc[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await write(path, stringifyYAML(config));
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async overrideValue(name: string, key: string, value: string) {
|
||||||
|
const path = this.composePath(name);
|
||||||
|
const f = file(path);
|
||||||
|
if (!(await f.exists())) throw new Error('Project not found');
|
||||||
|
const text = await f.text();
|
||||||
|
const config = Bun.YAML.parse(text);
|
||||||
|
|
||||||
|
if (forbiddenKeys.has(key)) throw new Error(`Key ${key} cannot be overridden`);
|
||||||
|
if (!config.services.mc.environment) config.services.mc.environment = {};
|
||||||
|
config.services.mc.environment[key] = value;
|
||||||
|
|
||||||
|
await write(path, stringifyYAML(config));
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeProject(name: string) {
|
||||||
|
const cwd = this.projectPath(name);
|
||||||
|
await spawn(['docker', 'compose', 'down', '-v'], { cwd }).exited;
|
||||||
|
await $`rm -rf ${cwd}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async runCompose(name: string, args: string[]) {
|
||||||
|
const cwd = this.projectPath(name);
|
||||||
|
const result = await spawn(['docker', 'compose', ...args], { cwd });
|
||||||
|
return result.exited;
|
||||||
|
}
|
||||||
|
|
||||||
|
async attach(name: string) {
|
||||||
|
const cwd = this.projectPath(name);
|
||||||
|
|
||||||
|
const process = spawn(['docker', 'exec', '-i', 'minecraft', 'rcon-cli'], {
|
||||||
|
cwd,
|
||||||
|
stdin: 'pipe',
|
||||||
|
stdout: 'pipe',
|
||||||
|
stderr: 'pipe'
|
||||||
|
});
|
||||||
|
|
||||||
|
const ansiColorMap: Record<string, string> = {
|
||||||
|
'30': 'black',
|
||||||
|
'31': 'red',
|
||||||
|
'32': 'green',
|
||||||
|
'33': 'yellow',
|
||||||
|
'34': 'blue',
|
||||||
|
'35': 'magenta',
|
||||||
|
'36': 'cyan',
|
||||||
|
'37': 'white',
|
||||||
|
'90': 'brightBlack',
|
||||||
|
'91': 'brightRed',
|
||||||
|
'92': 'brightGreen',
|
||||||
|
'93': 'brightYellow',
|
||||||
|
'94': 'brightBlue',
|
||||||
|
'95': 'brightMagenta',
|
||||||
|
'96': 'brightCyan',
|
||||||
|
'97': 'brightWhite'
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseAnsiColors(text: string) {
|
||||||
|
const ansiRegex = /\x1b\[([0-9;]+)m/g;
|
||||||
|
const chunks: any[] = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
let currentColor: string | undefined;
|
||||||
|
let currentBold = false;
|
||||||
|
|
||||||
|
while ((match = ansiRegex.exec(text)) !== null) {
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
const textChunk = text.substring(lastIndex, match.index);
|
||||||
|
chunks.push({
|
||||||
|
text: textChunk,
|
||||||
|
color: currentColor,
|
||||||
|
bold: currentBold
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const codes = match[1].split(';');
|
||||||
|
for (const code of codes) {
|
||||||
|
if (code === '0') {
|
||||||
|
currentColor = undefined;
|
||||||
|
currentBold = false;
|
||||||
|
chunks.push({ text: '', reset: true });
|
||||||
|
} else if (code === '1') {
|
||||||
|
currentBold = true;
|
||||||
|
} else if (ansiColorMap[code]) {
|
||||||
|
currentColor = ansiColorMap[code];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = ansiRegex.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
const textChunk = text.substring(lastIndex);
|
||||||
|
chunks.push({
|
||||||
|
text: textChunk,
|
||||||
|
color: currentColor,
|
||||||
|
bold: currentBold
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
const processOutput = async (stream: ReadableStream<Uint8Array>) => {
|
||||||
|
const reader = stream.getReader();
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const colorChunks = parseAnsiColors(line);
|
||||||
|
|
||||||
|
let formattedText = '';
|
||||||
|
for (const chunk of colorChunks) {
|
||||||
|
if (chunk.reset) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = chunk.text;
|
||||||
|
if (text) {
|
||||||
|
if (chunk.color) {
|
||||||
|
if (chunk.bold) {
|
||||||
|
text = `<${chunk.color} font-bold>${text}</${chunk.color}>`;
|
||||||
|
} else {
|
||||||
|
text = `<${chunk.color}>${text}</${chunk.color}>`;
|
||||||
|
}
|
||||||
|
} else if (chunk.bold) {
|
||||||
|
text = `<span font-bold>${text}</span>`;
|
||||||
|
}
|
||||||
|
formattedText += text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.enqueue({
|
||||||
|
type: 'output',
|
||||||
|
formatted: formattedText,
|
||||||
|
chunks: colorChunks,
|
||||||
|
rawText: line
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
controller.error(error);
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.stdout) {
|
||||||
|
processOutput(process.stdout);
|
||||||
|
}
|
||||||
|
if (process.stderr) {
|
||||||
|
processOutput(process.stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const writer = {
|
||||||
|
async write(text: string) {
|
||||||
|
if (process.stdin) {
|
||||||
|
await process.stdin.write(text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async close() {
|
||||||
|
if (process.stdin) {
|
||||||
|
await process.stdin.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
reader,
|
||||||
|
writer,
|
||||||
|
process
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ProjectManager };
|
8
app/src/lib/auth-client.ts
Normal file
8
app/src/lib/auth-client.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createAuthClient } from 'better-auth/svelte';
|
||||||
|
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
baseUrl: '/api',
|
||||||
|
fetchOptions: {
|
||||||
|
credentials: 'include'
|
||||||
|
}
|
||||||
|
});
|
Reference in New Issue
Block a user