Leave CLI as broken

This commit is contained in:
Jesse Wierzbinski 2024-03-10 19:30:26 -10:00
parent b69f20ccf4
commit f4fd16179c
No known key found for this signature in database
6 changed files with 350 additions and 77 deletions

View file

@ -4,14 +4,11 @@
![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) ![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white) ![VS Code Insiders](https://img.shields.io/badge/VS%20Code%20Insiders-35b393.svg?style=for-the-badge&logo=visual-studio-code&logoColor=white) ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) ![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black) ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) ![ESLint](https://img.shields.io/badge/ESLint-4B3263?style=for-the-badge&logo=eslint&logoColor=white) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa?style=for-the-badge)](code_of_conduct.md)
> [!IMPORTANT]
> This project is **not abandoned**, my laptop merely broke and I am waiting for a new one to arrive
## What is this?
This is a project to create a federated social network based on the [Lysand](https://lysand.org) protocol. It is currently in alpha phase, with basic federation and API support.
This is a project to create a federated social network based on the [Lysand](https://lysand.org) protocol. It is currently in alpha phase, with basic federation and almost complete Mastodon API support.
This project aims to be a fully featured social network, with a focus on privacy, security, and performance. It will implement the Mastodon API for support with clients that already support Mastodon or Pleroma.
This project aims to be a fully featured social network, with a focus on privacy, security, and performance. It implements the Mastodon API for support with clients that already support Mastodon or Pleroma.
> [!NOTE]
> This project is not affiliated with Mastodon or Pleroma, and is not a fork of either project. It is a new project built from the ground up.
@ -35,7 +32,7 @@ This project aims to be a fully featured social network, with a focus on privacy
## Benchmarks
> [!NOTE]
> These benchmarks are not representative of real-world performance, and are only meant to be used as a rough guide.
> These benchmarks are not representative of real-world performance, and are only meant to be used as a rough guide. Load, and therefore performance, will vary depending on the server's hardware and software configuration, as well as user activity.
### Timeline Benchmarks
@ -67,18 +64,21 @@ $ bun run benchmarks/timelines.ts 10000
✓ 10000 requests fulfilled in 12.44852s
```
Lysand is extremely fast and can handle tens of thousands of HTTP requests per second on a good server.
Lysand is extremely fast and can handle thousands of HTTP requests per second on a good server.
## How do I run it?
### Requirements
- The [Bun Runtime](https://bun.sh), version 1.0.5 or later (usage of the latest version is recommended)
- The [Bun Runtime](https://bun.sh), version 1.0.30 or later (usage of the latest version is recommended)
- A PostgreSQL database
- (Optional but recommended) A Linux-based operating system
- (Optional if you want search) A working Meiliseach instance
> **Note**: We will not be offerring support to Windows or MacOS users. If you are using one of these operating systems, please use a virtual machine or container to run Lysand.
> [!WARNING]
> Lysand has not been tested on Windows or MacOS. It is recommended to use a Linux-based operating system to run Lysand.
>
> We will not be offerring support to Windows or MacOS users. If you are using one of these operating systems, please use a virtual machine or container to run Lysand.
### Installation
@ -152,6 +152,9 @@ bun start
### Using the CLI
> [!WARNING]
> The CLI is currently broken due to unknown bugs that are actively being investigated. The following instructions are for when this is fixed.
Lysand includes a built-in CLI for managing the server. To use it, simply run the following command:
```bash
@ -279,10 +282,12 @@ Working endpoints are:
- `/api/v1/blocks`
- `/api/v1/mutes`
- `/api/v2/media`
- `/api/v1/notifications`
Tests needed but completed:
- `/api/v1/media/:id`
- `/api/v2/media`
- `/api/v1/favourites`
- `/api/v1/accounts/:id/followers`
- `/api/v1/accounts/:id/following`
@ -335,7 +340,6 @@ Endpoints left:
- `/api/v1/lists/:id` (`GET`, `PUT`, `DELETE`)
- `/api/v1/markers` (`GET`, `POST`)
- `/api/v1/lists/:id/accounts` (`GET`, `POST`, `DELETE`)
- `/api/v1/notifications`
- `/api/v1/notifications/:id`
- `/api/v1/notifications/clear`
- `/api/v1/notifications/:id/dismiss`

127
cli.ts
View file

@ -1,23 +1,139 @@
import type { Prisma } from "@prisma/client";
import chalk from "chalk";
import { client } from "~database/datasource";
import { createNewLocalUser } from "~database/entities/User";
import Table from "cli-table";
import { rebuildSearchIndexes, MeiliIndexType } from "@meilisearch";
import { getConfig } from "~classes/configmanager";
import { uploadFile } from "~classes/media";
import { getUrl } from "~database/entities/Attachment";
import { mkdir, exists } from "fs/promises";
import extract from "extract-zip";
import { client } from "~database/datasource";
import { CliBuilder, CliCommand } from "cli-parser";
import { CliParameterType } from "~packages/cli-parser/cli-builder.type";
import { PrismaClient } from "@prisma/client";
import { ConfigManager } from "~packages/config-manager";
const args = process.argv;
const config = await new ConfigManager({}).getConfig();
console.error("CLI is temporarily broken, please use the Prisma CLI instead");
process.exit(1);
const cliBuilder = new CliBuilder([
new CliCommand(
["help"],
[],
() => {
cliBuilder.displayHelp();
},
"Shows help for the CLI"
),
new CliCommand<{
username: string;
password: string;
email: string;
admin: boolean;
help: boolean;
}>(
["user", "create"],
[
{
name: "username",
type: CliParameterType.STRING,
description: "Username of the user",
needsValue: true,
positioned: false,
},
{
name: "password",
type: CliParameterType.STRING,
description: "Password of the user",
needsValue: true,
positioned: false,
},
{
name: "email",
type: CliParameterType.STRING,
description: "Email of the user",
needsValue: true,
positioned: false,
},
{
name: "admin",
type: CliParameterType.BOOLEAN,
description: "Make the user an admin",
needsValue: false,
positioned: false,
},
{
name: "help",
shortName: "h",
type: CliParameterType.EMPTY,
description: "Show help message",
needsValue: false,
positioned: false,
},
],
(instance: CliCommand, args) => {
const { username, password, email, admin, help } = args;
if (help) {
instance.displayHelp();
return;
}
// Check if username, password and email are provided
if (!username || !password || !email) {
console.log(
`${chalk.red(``)} Missing username, password or email`
);
return;
}
// Check if user already exists
void client.user
.findFirst({
where: {
OR: [{ username }, { email }],
},
})
.then(user => {
if (user) {
console.log(`${chalk.red(``)} User already exists`);
return;
}
console.log("Sus");
// Create user
/* const newUser = await createNewLocalUser({
email: email,
password: password,
username: username,
admin: admin,
});
console.log(
`${chalk.green(``)} Created user ${chalk.blue(
newUser.username
)}${admin ? chalk.green(" (admin)") : ""}`
); */
});
},
"Creates a new user",
"bun cli user create --username admin --password password123 --email email@email.com"
),
]);
cliBuilder.processArgs(args);
process.exit(0);
/**
* Make the text have a width of 20 characters, padding with gray dots
* Text can be a Chalk string, in which case formatting codes should not be counted in text length
* @param text The text to align
*/
const alignDots = (text: string, length = 20) => {
/* const alignDots = (text: string, length = 20) => {
// Remove formatting codes
// eslint-disable-next-line no-control-regex
const textLength = text.replace(/\u001b\[\d+m/g, "").length;
@ -1065,3 +1181,4 @@ switch (command) {
}
process.exit(0);
*/

View file

@ -1,10 +1,23 @@
export interface CliParameter {
name: string;
// If not positioned, the argument will need to be called with --name value instead of just value
/* Like -v for --version */
shortName?: string;
/**
* If not positioned, the argument will need to be called with --name value instead of just value
* @default true
*/
positioned?: boolean;
// Whether the argument needs a value (requires positioned to be false)
/* Whether the argument needs a value (requires positioned to be false) */
needsValue?: boolean;
optional?: true;
type: "string" | "number" | "boolean" | "array";
type: CliParameterType;
description?: string;
}
export enum CliParameterType {
STRING = "string",
NUMBER = "number",
BOOLEAN = "boolean",
ARRAY = "array",
EMPTY = "empty",
}

View file

@ -1,4 +1,4 @@
import type { CliParameter } from "./cli-builder.type";
import { CliParameterType, type CliParameter } from "./cli-builder.type";
import chalk from "chalk";
export function startsWithArray(fullArray: any[], startArray: any[]) {
@ -193,21 +193,21 @@ export class CliBuilder {
if (value instanceof CliCommand) {
writeBuffer += `${" ".repeat(depth)}${chalk.blue(key)}|${chalk.underline(value.description)}\n`;
const positionedArgs = value.argTypes.filter(
arg => arg.positioned
arg => arg.positioned ?? true
);
const unpositionedArgs = value.argTypes.filter(
arg => !arg.positioned
arg => !(arg.positioned ?? true)
);
for (const arg of unpositionedArgs) {
for (const arg of positionedArgs) {
writeBuffer += `${" ".repeat(depth + 1)}${chalk.green(
arg.name
)}|${
arg.description ?? "(no description)"
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
}
for (const arg of positionedArgs) {
writeBuffer += `${" ".repeat(depth + 1)}${chalk.yellow("--" + arg.name)}|${
for (const arg of unpositionedArgs) {
writeBuffer += `${" ".repeat(depth + 1)}${chalk.yellow("--" + arg.name)}${arg.shortName ? ", " + chalk.yellow("-" + arg.shortName) : ""}|${
arg.description ?? "(no description)"
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
}
@ -253,15 +253,38 @@ export class CliBuilder {
}
}
/* type CliParametersToType<T extends CliParameter[]> = {
[K in T[number]["name"]]: T[number]["type"] extends CliParameterType.STRING
? string
: T[number]["type"] extends CliParameterType.NUMBER
? number
: T[number]["type"] extends CliParameterType.BOOLEAN
? boolean
: T[number]["type"] extends CliParameterType.ARRAY
? string[]
: T[number]["type"] extends CliParameterType.EMPTY
? never
: never;
};
type ExecuteFunction<T extends CliParameter[]> = (
args: CliParametersToType<T>
) => void; */
type ExecuteFunction<T> = (
instance: CliCommand,
args: Partial<T>
) => Promise<void> | void;
/**
* A command that can be executed from the command line
* @param categories Example: `["user", "create"]` for the command `./cli user create --name John`
*/
export class CliCommand {
export class CliCommand<T = any> {
constructor(
public categories: string[],
public argTypes: CliParameter[],
private execute: (args: Record<string, any>) => void,
private execute: ExecuteFunction<T>,
public description?: string,
public example?: string
) {}
@ -271,13 +294,17 @@ export class CliCommand {
* formatted with Chalk and with emojis
*/
displayHelp() {
const positionedArgs = this.argTypes.filter(arg => arg.positioned);
const unpositionedArgs = this.argTypes.filter(arg => !arg.positioned);
const positionedArgs = this.argTypes.filter(
arg => arg.positioned ?? true
);
const unpositionedArgs = this.argTypes.filter(
arg => !(arg.positioned ?? true)
);
const helpMessage = `
${chalk.green("📚 Command:")} ${chalk.yellow(this.categories.join(" "))}
${this.description ? `${chalk.cyan(this.description)}\n` : ""}
${chalk.magenta("🔧 Arguments:")}
${unpositionedArgs
${positionedArgs
.map(
arg =>
`${chalk.bold(arg.name)}: ${chalk.blue(arg.description ?? "(no description)")} ${
@ -285,10 +312,10 @@ ${unpositionedArgs
}`
)
.join("\n")}
${positionedArgs
${unpositionedArgs
.map(
arg =>
`--${chalk.bold(arg.name)}: ${chalk.blue(arg.description ?? "(no description)")} ${
`--${chalk.bold(arg.name)}${arg.shortName ? `, -${arg.shortName}` : ""}: ${chalk.blue(arg.description ?? "(no description)")} ${
arg.optional ? chalk.gray("(optional)") : ""
}`
)
@ -328,6 +355,20 @@ ${positionedArgs
i++;
currentParameter = null;
}
} else if (arg.startsWith("-")) {
const shortName = arg.substring(1);
const argType = this.argTypes.find(
argType => argType.shortName === shortName
);
if (argType && !argType.needsValue) {
parsedArgs[argType.name] = true;
} else if (argType && argType.needsValue) {
parsedArgs[argType.name] = this.castArgValue(
argsWithoutCategories[i + 1],
argType.type
);
i++;
}
} else if (currentParameter) {
parsedArgs[currentParameter.name] = this.castArgValue(
arg,
@ -352,13 +393,13 @@ ${positionedArgs
private castArgValue(value: string, type: CliParameter["type"]): any {
switch (type) {
case "string":
case CliParameterType.STRING:
return value;
case "number":
case CliParameterType.NUMBER:
return Number(value);
case "boolean":
case CliParameterType.BOOLEAN:
return value === "true";
case "array":
case CliParameterType.ARRAY:
return value.split(",");
default:
return value;
@ -370,6 +411,6 @@ ${positionedArgs
*/
run(argsWithoutCategories: string[]) {
const args = this.parseArgs(argsWithoutCategories);
this.execute(args);
void this.execute(this, args as any);
}
}

View file

@ -2,6 +2,7 @@
import { CliCommand, CliBuilder, startsWithArray } from "..";
import { describe, beforeEach, it, expect, jest, spyOn } from "bun:test";
import stripAnsi from "strip-ansi";
import { CliParameterType } from "../cli-builder.type";
describe("startsWithArray", () => {
it("should return true when fullArray starts with startArray", () => {
@ -36,10 +37,27 @@ describe("CliCommand", () => {
cliCommand = new CliCommand(
["category1", "category2"],
[
{ name: "arg1", type: "string", needsValue: true },
{ name: "arg2", type: "number", needsValue: true },
{ name: "arg3", type: "boolean", needsValue: false },
{ name: "arg4", type: "array", needsValue: true },
{
name: "arg1",
type: CliParameterType.STRING,
needsValue: true,
},
{
name: "arg2",
shortName: "a",
type: CliParameterType.NUMBER,
needsValue: true,
},
{
name: "arg3",
type: CliParameterType.BOOLEAN,
needsValue: false,
},
{
name: "arg4",
type: CliParameterType.ARRAY,
needsValue: true,
},
],
() => {
// Do nothing
@ -65,13 +83,34 @@ describe("CliCommand", () => {
});
});
it("should cast argument values correctly", () => {
expect(cliCommand["castArgValue"]("42", "number")).toBe(42);
expect(cliCommand["castArgValue"]("true", "boolean")).toBe(true);
expect(cliCommand["castArgValue"]("value1,value2", "array")).toEqual([
it("should parse short names for arguments too", () => {
const args = cliCommand["parseArgs"]([
"--arg1",
"value1",
"value2",
"-a",
"42",
"--arg3",
"--arg4",
"value1,value2",
]);
expect(args).toEqual({
arg1: "value1",
arg2: 42,
arg3: true,
arg4: ["value1", "value2"],
});
});
it("should cast argument values correctly", () => {
expect(cliCommand["castArgValue"]("42", CliParameterType.NUMBER)).toBe(
42
);
expect(
cliCommand["castArgValue"]("true", CliParameterType.BOOLEAN)
).toBe(true);
expect(
cliCommand["castArgValue"]("value1,value2", CliParameterType.ARRAY)
).toEqual(["value1", "value2"]);
});
it("should run the execute function with the parsed parameters", () => {
@ -79,10 +118,26 @@ describe("CliCommand", () => {
cliCommand = new CliCommand(
["category1", "category2"],
[
{ name: "arg1", type: "string", needsValue: true },
{ name: "arg2", type: "number", needsValue: true },
{ name: "arg3", type: "boolean", needsValue: false },
{ name: "arg4", type: "array", needsValue: true },
{
name: "arg1",
type: CliParameterType.STRING,
needsValue: true,
},
{
name: "arg2",
type: CliParameterType.NUMBER,
needsValue: true,
},
{
name: "arg3",
type: CliParameterType.BOOLEAN,
needsValue: false,
},
{
name: "arg4",
type: CliParameterType.ARRAY,
needsValue: true,
},
],
mockExecute
);
@ -109,13 +164,29 @@ describe("CliCommand", () => {
cliCommand = new CliCommand(
["category1", "category2"],
[
{ name: "arg1", type: "string", needsValue: true },
{ name: "arg2", type: "number", needsValue: true },
{ name: "arg3", type: "boolean", needsValue: false },
{ name: "arg4", type: "array", needsValue: true },
{
name: "arg1",
type: CliParameterType.STRING,
needsValue: true,
},
{
name: "arg2",
type: CliParameterType.NUMBER,
needsValue: true,
},
{
name: "arg3",
type: CliParameterType.BOOLEAN,
needsValue: false,
},
{
name: "arg4",
type: CliParameterType.ARRAY,
needsValue: true,
},
{
name: "arg5",
type: "string",
type: CliParameterType.STRING,
needsValue: true,
positioned: true,
},
@ -153,31 +224,31 @@ describe("CliCommand", () => {
[
{
name: "arg1",
type: "string",
type: CliParameterType.STRING,
needsValue: true,
description: "Argument 1",
optional: true,
},
{
name: "arg2",
type: "number",
type: CliParameterType.NUMBER,
needsValue: true,
description: "Argument 2",
},
{
name: "arg3",
type: "boolean",
type: CliParameterType.BOOLEAN,
needsValue: false,
description: "Argument 3",
optional: true,
positioned: true,
positioned: false,
},
{
name: "arg4",
type: "array",
type: CliParameterType.ARRAY,
needsValue: true,
description: "Argument 4",
positioned: true,
positioned: false,
},
],
() => {
@ -260,7 +331,7 @@ describe("CliBuilder", () => {
[
{
name: "arg1",
type: "string",
type: CliParameterType.STRING,
needsValue: true,
positioned: false,
},
@ -352,20 +423,28 @@ describe("CliBuilder", () => {
[
{
name: "name",
type: "string",
type: CliParameterType.STRING,
needsValue: true,
description: "Name of new item",
},
{
name: "delete-previous",
type: "number",
type: CliParameterType.NUMBER,
needsValue: false,
positioned: true,
positioned: false,
optional: true,
description: "Also delete the previous item",
},
{ name: "arg3", type: "boolean", needsValue: false },
{ name: "arg4", type: "array", needsValue: true },
{
name: "arg3",
type: CliParameterType.BOOLEAN,
needsValue: false,
},
{
name: "arg4",
type: CliParameterType.ARRAY,
needsValue: true,
},
],
() => {
// Do nothing

View file

@ -75,7 +75,11 @@ export const createServer = (
return errorResponse("Route not found", 500);
}
if (matchedRoute && file != undefined) {
if (
matchedRoute &&
matchedRoute.name !== "/[...404]" &&
file != undefined
) {
const meta = file.meta;
// Check for allowed requests
@ -133,35 +137,50 @@ export const createServer = (
configManager,
parsedRequest,
});
} else {
} else if (matchedRoute?.name === "/[...404]") {
// Proxy response from Vite at localhost:5173 if in development mode
if (isProd) {
if (new URL(req.url).pathname.startsWith("/assets")) {
// Serve from pages/dist/assets
return new Response(
Bun.file(`./pages/dist${new URL(req.url).pathname}`)
const file = Bun.file(
`./pages/dist${new URL(req.url).pathname}`
);
// Serve from pages/dist/assets
if (await file.exists()) {
return new Response(file);
} else return errorResponse("Asset not found", 404);
}
if (new URL(req.url).pathname.startsWith("/api")) {
return errorResponse("Route not found", 404);
}
const file = Bun.file(`./pages/dist/index.html`);
// Serve from pages/dist
return new Response(Bun.file(`./pages/dist/index.html`));
return new Response(file);
} else {
const proxy = await fetch(
req.url.replace(
config.http.base_url,
"http://localhost:5173"
)
);
).catch(async e => {
await logger.logError(
LogLevel.ERROR,
"Server.Proxy",
e as Error
);
return errorResponse("Route not found", 404);
});
if (proxy.status !== 404) {
return proxy;
}
}
return new Response(undefined, {
status: 404,
statusText: "Route not found",
});
return errorResponse("Route not found", 404);
} else {
return errorResponse("Route not found", 404);
}
},
});