server init

This commit is contained in:
Oleksandr Honcharov
2025-02-10 13:54:47 +02:00
commit b6ec8de3ba
20 changed files with 697 additions and 0 deletions
@@ -0,0 +1,37 @@
import chalk from "chalk";
import { commandHandler, helpers } from "../core";
import type { Command } from "../core";
const changeProfileEnabled: Command = {
name: "change-profile-enabled",
description:
"Enable profile with the given name. Usage: change-profile-enabled [true/false] <profile name>",
execute: async (args: string[]) => {
if (args.length < 2) {
console.log(chalk.red("Please provide profile status and name"));
return;
}
const status = String(args[0]).toLowerCase() === "true";
const profileName = args.slice(1).join(" ");
const profiles = await helpers.profile.getAll();
const profile = profiles.find((profile) => profile.name === profileName);
if (!profile) {
console.log(chalk.red(`Profile "${profileName}" not found`));
return;
}
profile.enabled = status;
await helpers.profile.save(profile.id, JSON.stringify(profile));
console.log(
chalk.yellow(
`Profile "${profileName}" is now ${status ? chalk.green("enabled") : chalk.red("disabled")}`,
),
);
},
};
commandHandler.registerCommand(changeProfileEnabled);
@@ -0,0 +1,39 @@
import { v4 as uuidv4 } from "uuid";
import chalk from "chalk";
import { commandHandler, helpers } from "../core";
import type { Command } from "../core";
import type { ProfileFile } from "./types";
const createProfileCommand: Command = {
name: "create-profile",
description:
"Creates a new profile with the given name. Usage: create-profile <profile name>",
execute: async (args: string[]) => {
if (args.length < 1) {
console.log(chalk.red("Please provide a profile name"));
return;
}
const profileName = args.join(" ");
console.log(chalk.yellow(`Creating profile for ${profileName}...`));
const id = uuidv4();
const assetsFolder = profileName.split(" ").join("-").toLowerCase();
const profileData: ProfileFile = {
id,
name: profileName,
assetsFolder,
enabled: false,
};
await helpers.profile.save(id, JSON.stringify(profileData));
await helpers.asset.init(assetsFolder);
console.log(chalk.green(`Profile "${profileName}" created with id: ${id}`));
},
};
commandHandler.registerCommand(createProfileCommand);
+3
View File
@@ -0,0 +1,3 @@
import "./createProfile";
import "./syncup";
import "./changeProfileEnabled";
+26
View File
@@ -0,0 +1,26 @@
import { readdir } from "node:fs/promises";
import * as path from "node:path";
import chalk from "chalk";
import { commandHandler, helpers } from "../core";
import { ASSETS_FOLDER, PROFILES_FOLDER } from "../consts";
import type { ProfileFile } from "./types";
import type { Command } from "../core";
const syncUpCommand: Command = {
name: "sync-up",
description: "Syncs up all assets folders with the manifest. Usage: sync-up",
execute: async (args: string[]) => {
const profileName = args.slice(1).join(" ");
const profiles = await helpers.profile.getAll();
const profile = profiles.find((profile) => profile.name === profileName);
if (!profile) {
console.log(chalk.red(`Profile "${profileName}" not found`));
return;
}
},
};
commandHandler.registerCommand(syncUpCommand);
+41
View File
@@ -0,0 +1,41 @@
import { readdir } from "node:fs/promises";
import * as path from "node:path";
import chalk from "chalk";
import { commandHandler, helpers } from "../core";
import { ASSETS_FOLDER, PROFILES_FOLDER } from "../consts";
import type { ProfileFile } from "./types";
import type { Command } from "../core";
const syncUpCommand: Command = {
name: "sync-up",
description: "Syncs up all assets folders with the manifest. Usage: sync-up",
execute: async () => {
if (await helpers.manifest.isFolderExists()) {
const dirEntries = await readdir(PROFILES_FOLDER, {
withFileTypes: true,
});
for (const entry of dirEntries) {
const file = Bun.file(path.join(PROFILES_FOLDER, entry.name));
const data: ProfileFile = JSON.parse(await file.text());
console.log(chalk.green(`Syncing up profile: ${data.id}`));
const pathToAssets = path.join(ASSETS_FOLDER, data.assetsFolder);
const generatedManifest = await helpers.manifest.generate(pathToAssets);
await helpers.manifest.save(
data.id,
JSON.stringify(generatedManifest),
await helpers.manifest.isExists(data.id),
);
}
console.log(chalk.green("Sync up complete!"));
}
},
};
commandHandler.registerCommand(syncUpCommand);
@@ -0,0 +1,11 @@
export interface ProfileFile {
id: string;
name: string;
enabled: boolean;
assetsFolder: string;
}
export interface ManifestFile {
path: string;
hash: string;
}
+5
View File
@@ -0,0 +1,5 @@
import * as path from "node:path";
export const MANIFEST_FOLDER = path.join(process.cwd(), "manifests");
export const PROFILES_FOLDER = path.join(process.cwd(), "profiles");
export const ASSETS_FOLDER = path.join(process.cwd(), "assets");
@@ -0,0 +1,92 @@
import readline from "readline";
import chalk from "chalk";
export interface Command {
name: string;
description: string;
execute: (args: string[]) => void | Promise<void>;
}
class CommandHandler {
private commands: Map<string, Command>;
private rl: readline.Interface;
constructor() {
this.commands = new Map();
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: chalk.blue("> "),
});
this.registerCommand({
name: "help",
description: "Shows all available commands",
execute: this.handleHelp.bind(this),
});
this.registerCommand({
name: "exit",
description: "Exits the application",
execute: () => {
console.log(chalk.yellow("Goodbye!"));
this.rl.close();
process.exit(0);
},
});
}
public registerCommand(command: Command): void {
this.commands.set(command.name, command);
}
private handleHelp(): void {
console.log(chalk.green("\nAvailable commands:"));
this.commands.forEach((command) => {
console.log(chalk.cyan(`${command.name}: `) + command.description);
});
console.log();
}
private async executeCommand(input: string): Promise<void> {
const args = input.trim().split(/\s+/);
const commandName = args.shift()?.toLowerCase();
if (!commandName) {
return;
}
const command = this.commands.get(commandName);
if (!command) {
console.log(chalk.red(`Unknown command: ${commandName}`));
console.log(chalk.yellow('Type "help" to see available commands'));
return;
}
try {
await command.execute(args);
} catch (error) {
console.error(chalk.red("Error executing command:"), error);
}
}
public start(): void {
console.log(chalk.green("Welcome to the SkyMP LaunchServer CLI!"));
console.log(chalk.yellow('Type "help" to see available commands\n'));
this.rl.prompt();
this.rl.on("line", async (input) => {
await this.executeCommand(input.trim());
this.rl.prompt();
});
this.rl.on("close", () => {
console.log(chalk.yellow("Goodbye!"));
process.exit(0);
});
}
}
export const commandHandler = new CommandHandler();
+27
View File
@@ -0,0 +1,27 @@
import { Elysia } from "elysia";
import chalk from "chalk";
import staticPlugin from "@elysiajs/static";
import { helpers } from "./helpers.ts";
new Elysia()
.use(
staticPlugin({
prefix: "/api/assets",
assets: "assets",
}),
)
.group("/api", (group) => {
return group
.get("/profiles", async () => {
const allProfiles = await helpers.profile.getAll();
return allProfiles.filter((profile) => profile.enabled);
})
.get("/manifest/:id", async ({ params: { id } }) => {
return await helpers.manifest.get(id);
});
})
.listen(5454);
console.log(chalk.green("Server started on port 5454"));
+149
View File
@@ -0,0 +1,149 @@
import { exists, mkdir, readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
import * as crypto from "node:crypto";
import { ASSETS_FOLDER, MANIFEST_FOLDER, PROFILES_FOLDER } from "../consts";
import type { ManifestFile, ProfileFile } from "../commands/types";
export interface FileInfo {
path: string;
hash: string;
size: number;
}
export const helpers = {
manifest: {
init: async () => {
if (!(await exists(MANIFEST_FOLDER))) {
try {
await mkdir(MANIFEST_FOLDER, { recursive: true });
} catch (e) {
console.error(e);
}
}
},
isFolderExists: async () => {
if (await exists(MANIFEST_FOLDER)) {
return true;
}
await helpers.manifest.init();
return true;
},
isExists: async (id: string) => {
const manifestPath = path.join(MANIFEST_FOLDER, `${id}.json`);
return await exists(manifestPath);
},
save: async (id: string, data: string, resave: boolean) => {
if (resave) {
await Bun.file(path.join(MANIFEST_FOLDER, `${id}.json`)).delete();
}
await Bun.write(path.join(MANIFEST_FOLDER, `${id}.json`), data);
},
generate: async (dir: string): Promise<FileInfo[]> => {
const files: FileInfo[] = [];
async function scanDir(currentDir: string) {
const dirEntries = await readdir(currentDir, { withFileTypes: true });
for (const entry of dirEntries) {
const fullPath = path.join(currentDir, entry.name);
const relPath = path.relative(dir, fullPath);
if (entry.isDirectory()) {
await scanDir(fullPath);
} else if (entry.isFile()) {
const fileBuffer = await readFile(fullPath);
const hash = crypto
.createHash("sha256")
.update(fileBuffer)
.digest("hex");
const { size } = await stat(fullPath);
files.push({ path: relPath, hash, size });
}
}
}
await scanDir(dir);
return files;
},
get: async (id: string): Promise<ManifestFile[]> => {
const isProfileEnabled = await helpers.profile.enabled(id);
if (!isProfileEnabled) return [];
const manifestFilePath = path.join(MANIFEST_FOLDER, `${id}.json`);
const file = Bun.file(manifestFilePath);
return JSON.parse(await file.text());
},
},
profile: {
save: async (name: string, data: string) => {
if (!(await exists(PROFILES_FOLDER))) {
await mkdir(PROFILES_FOLDER, { recursive: true });
}
await Bun.write(path.join(PROFILES_FOLDER, `${name}.json`), data);
},
getAll: async (): Promise<ProfileFile[]> => {
const dirEntries = await readdir(PROFILES_FOLDER, {
withFileTypes: true,
});
const profiles = [];
for (const entry of dirEntries) {
const file = Bun.file(path.join(PROFILES_FOLDER, entry.name));
const data: ProfileFile = JSON.parse(await file.text());
profiles.push(data);
}
return profiles;
},
enabled: async (id: string): Promise<Boolean> => {
const profilePath = path.join(PROFILES_FOLDER, `${id}.json`);
if (!(await exists(profilePath))) return false;
const file = Bun.file(profilePath);
const data: ProfileFile = JSON.parse(await file.text());
return data.enabled;
},
},
asset: {
init: async (dir: string) => {
const targetPath = path.join(ASSETS_FOLDER, dir);
if (await exists(targetPath)) {
return;
}
try {
await mkdir(targetPath, { recursive: true });
} catch (e) {
console.error(e);
}
await Bun.write(
path.join(targetPath, "your-assets-here.txt"),
"Place your assets here",
);
},
},
system: {
init: async () => {
if (!(await exists(ASSETS_FOLDER))) {
await mkdir(ASSETS_FOLDER, { recursive: true });
}
if (!(await exists(PROFILES_FOLDER))) {
await mkdir(PROFILES_FOLDER, { recursive: true });
}
if (!(await exists(MANIFEST_FOLDER))) {
await mkdir(MANIFEST_FOLDER, { recursive: true });
}
},
},
};
+2
View File
@@ -0,0 +1,2 @@
export * from "./helpers.ts";
export * from "./commandHandler.ts";
+9
View File
@@ -0,0 +1,9 @@
import { helpers } from "./core";
import { commandHandler } from "./core";
import "./commands";
await helpers.system.init();
import "./core/elysia";
commandHandler.start();