diff --git a/package-lock.json b/package-lock.json index a49aa24..25ec052 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,16 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "minecraft-protocol": "^1.47.0" + "@types/text-table": "^0.2.5", + "chalk": "^5.3.0", + "commander": "^12.1.0", + "minecraft-protocol": "^1.47.0", + "strip-ansi": "^7.1.0", + "text-table": "^0.2.0", + "zod": "^3.23.8" + }, + "bin": { + "m3": "dist/m3/cli.js" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -306,6 +315,16 @@ "balanced-match": "^1.0.0" } }, + "node_modules/@swc/cli/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/@swc/cli/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -643,6 +662,12 @@ "@types/node": "*" } }, + "node_modules/@types/text-table": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@types/text-table/-/text-table-0.2.5.tgz", + "integrity": "sha512-hcZhlNvMkQG/k1vcZ6yHOl6WAYftQ2MLfTHcYRZ2xYZFD8tGVnE3qFV0lj1smQeDSR7/yY0PyuUalauf33bJeA==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", @@ -946,13 +971,15 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { @@ -1312,17 +1339,12 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -1400,13 +1422,12 @@ "license": "MIT" }, "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=18" } }, "node_modules/concat-map": { @@ -1664,6 +1685,46 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/espree": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", @@ -3681,16 +3742,18 @@ } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-eof": { @@ -3774,7 +3837,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, "license": "MIT" }, "node_modules/to-regex-range": { @@ -4045,6 +4107,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index f9d3ad0..d5c2f70 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,13 @@ } }, "dependencies": { - "minecraft-protocol": "^1.47.0" + "@types/text-table": "^0.2.5", + "chalk": "^5.3.0", + "commander": "^12.1.0", + "minecraft-protocol": "^1.47.0", + "strip-ansi": "^7.1.0", + "text-table": "^0.2.0", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -40,5 +46,8 @@ "scripty": "^2.1.1", "typescript": "^5.5.4", "typescript-eslint": "^8.3.0" + }, + "bin": { + "m3": "./dist/m3/bin.js" } } diff --git a/src/instance/index.ts b/src/instance/index.ts index 68da316..497a1e0 100644 --- a/src/instance/index.ts +++ b/src/instance/index.ts @@ -1,7 +1,6 @@ import chalk from 'chalk'; import { createClient, type ServerClient } from 'minecraft-protocol'; -import { dirname, resolve } from 'path'; -import { fileURLToPath } from 'url'; +import { resolve } from 'path'; import { Worker } from 'worker_threads'; import { TARGET_OPTIONS } from '../settings.js'; @@ -9,13 +8,8 @@ import { Channel } from '../util/channel.js'; import { importModulesGenerator } from '../util/import-modules.js'; import { type Message } from '../worker/parent.js'; -if (!('require' in globalThis)) { - globalThis.__filename = fileURLToPath(import.meta.url); - globalThis.__dirname = dirname(__filename); -} - -const WORKER_PATH = resolve(__dirname, '../worker'); -const MODULE_DIR_PATH = resolve(__dirname, '../module'); +const WORKER_PATH = resolve(import.meta.dirname, '../worker'); +const MODULES_DIR_PATH = resolve(import.meta.dirname, '../modules'); export class Instance { public readonly client; @@ -68,7 +62,7 @@ export class Instance { let moduleStart = NaN; for await (const module of importModulesGenerator( - MODULE_DIR_PATH, + MODULES_DIR_PATH, 'global.js', { pre(entry) { diff --git a/src/m3/README.md b/src/m3/README.md new file mode 100644 index 0000000..cb76708 --- /dev/null +++ b/src/m3/README.md @@ -0,0 +1,35 @@ + + +# MMP Module Manager + +- _aka_ MMM _aka_ M3 (official-est name) + +A CLI Tool to automagically manage your modules. + +(no more manually `git clone`-ing dependencies!) + + diff --git a/src/m3/bin.ts b/src/m3/bin.ts new file mode 100755 index 0000000..959e978 --- /dev/null +++ b/src/m3/bin.ts @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { resolve } from 'path'; + +import { importDirectory } from '../util/import.js'; +import program from './program.js'; + +await importDirectory(resolve(import.meta.dirname, 'command')); + +program.parse(process.argv); diff --git a/src/m3/command/list.ts b/src/m3/command/list.ts new file mode 100644 index 0000000..a426da2 --- /dev/null +++ b/src/m3/command/list.ts @@ -0,0 +1,32 @@ +import chalk from 'chalk'; +import { readdir } from 'fs/promises'; +import { resolve } from 'path'; + +import { createTable } from '../../util/table.js'; +import { Module } from '../module.js'; +import program, { modulesDir } from '../program.js'; + +program + .command('list') + .description('List installed modules') + .action(async () => { + const tableData: Record[] = []; + + for (const entry of await readdir(modulesDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + + try { + const modulePath = resolve(modulesDir, entry.name); + const module = await Module.fromDir(modulePath); + + tableData.push({ + [chalk.bold('Name')]: module.global.name, + [chalk.bold('Dependency?')]: module.local.manual ? 'no' : 'yes', + }); + } catch (error) { + console.error(error); + } + } + + console.info(createTable(tableData)); + }); diff --git a/src/m3/module.ts b/src/m3/module.ts new file mode 100644 index 0000000..9445616 --- /dev/null +++ b/src/m3/module.ts @@ -0,0 +1,44 @@ +import { readFile } from 'fs/promises'; +import { resolve } from 'path'; + +import type { GlobalModuleData, LocalModuleData } from './types.js'; +import { GlobalModuleDataSchema, LocalModuleDataSchema } from './types.js'; + +async function getLocalData(path: string): Promise { + try { + const raw = await readFile(path, 'utf8'); + const data = JSON.parse(raw) as unknown; + + return LocalModuleDataSchema.parse(data); + } catch (error) { + throw new Error(`Could not load local module data: ${error}`); + } +} + +async function getGlobalData(path: string): Promise { + try { + const raw = await readFile(path, 'utf8'); + const data = JSON.parse(raw) as unknown; + + return GlobalModuleDataSchema.parse(data); + } catch (error) { + throw new Error(`Could not load global module data: ${error}`); + } +} + +export class Module { + public static async fromDir(path: string): Promise { + const localPath = resolve(path, 'm3.local.json'); + const globalPath = resolve(path, 'm3.global.json'); + + return new Module( + await getLocalData(localPath), + await getGlobalData(globalPath), + ); + } + + constructor( + public readonly local: LocalModuleData, + public readonly global: GlobalModuleData, + ) {} +} diff --git a/src/m3/program.ts b/src/m3/program.ts new file mode 100644 index 0000000..7a4d2a9 --- /dev/null +++ b/src/m3/program.ts @@ -0,0 +1,47 @@ +import { Command } from 'commander'; +import { basename, resolve } from 'path'; + +const program = new Command(); +export default program; + +program.name('m3').description('MMP Module Manager'); + +// #region This doesn't look like it belongs here... + +// Should always be set +export let modulesDir: string; + +// If cwd is a module, +// cwd will be {modulesDir}/{moduleDir} +export let moduleDir: string | undefined; + +const cwd = process.cwd(); +const cwdn = basename(cwd); + +const parent = resolve('..'); + +// are we in a module? +// (parent is modules dir) +if (basename(parent) === 'modules') { + modulesDir = parent; + moduleDir = cwdn; +} else { + switch (cwdn) { + case 'modular-minecraft-proxy': + modulesDir = resolve('src/modules'); + break; + case 'src': + modulesDir = resolve('modules'); + break; + case 'modules': + modulesDir = resolve('.'); + break; + } +} + +// @ts-expect-error We are testing if modulesDir *hasn't* been set here +if (modulesDir === undefined) { + throw new Error('Could not locate modules directory'); +} + +// #endregion diff --git a/src/m3/types.ts b/src/m3/types.ts new file mode 100644 index 0000000..ed87b40 --- /dev/null +++ b/src/m3/types.ts @@ -0,0 +1,25 @@ +// Module directory structure +// +// ├─ [local.ts] TypeScript +// ├─ [global.ts] TypeScript +// ├─ m3.local.json JSON(LocalModuleData) +// ├─ m3.global.json JSON(GlobalModuleData) +// └─ ... + +import { z } from 'zod'; + +export const LocalModuleDataSchema = z.object({ + // manual -> required by the user + // auto -> a dependency to another module + manual: z.boolean(), +}); + +export type LocalModuleData = z.infer; + +export const GlobalModuleDataSchema = z.object({ + dependencies: z.array(z.string().url()), + // * This is different from package.json's name as it is required to be the same as the directory's + name: z.string(), +}); + +export type GlobalModuleData = z.infer; diff --git a/src/module/.gitignore b/src/module/.gitignore deleted file mode 100644 index 9cb2ccc..0000000 --- a/src/module/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -* -!.gitignore - -# internal modules -!proxy diff --git a/src/modules/.gitignore b/src/modules/.gitignore new file mode 100644 index 0000000..4befe1d --- /dev/null +++ b/src/modules/.gitignore @@ -0,0 +1,7 @@ +* +!.gitignore + +# for whatever reason, you need to unignore +# the directories AND files within them +!internal.* +!internal.*/** diff --git a/src/module/proxy/global.ts b/src/modules/internal.proxy/global.ts similarity index 97% rename from src/module/proxy/global.ts rename to src/modules/internal.proxy/global.ts index 3d0b771..0e8c009 100644 --- a/src/module/proxy/global.ts +++ b/src/modules/internal.proxy/global.ts @@ -8,7 +8,7 @@ export default async function (instance: Instance): Promise { const downstreamQueue: RawPacket[] = []; const upstreamQueue: RawPacket[] = []; - const channel = instance.createChannel('proxy'); + const channel = instance.createChannel('internal.proxy'); const client = instance.client; const server = instance.server; diff --git a/src/module/proxy/local.ts b/src/modules/internal.proxy/local.ts similarity index 94% rename from src/module/proxy/local.ts rename to src/modules/internal.proxy/local.ts index f0b2b86..da540d9 100644 --- a/src/module/proxy/local.ts +++ b/src/modules/internal.proxy/local.ts @@ -7,8 +7,7 @@ import { type Direction as Direction, type Message } from './shared.js'; export type PacketEventMap = Record AsyncVoid>; -// ? Should I export the channel -export const channel = createChannel('proxy'); +const channel = createChannel('internal.proxy'); function write(direction: Direction, packet: RawPacket): void { channel.write({ diff --git a/src/modules/internal.proxy/m3.global.json b/src/modules/internal.proxy/m3.global.json new file mode 100644 index 0000000..b10a046 --- /dev/null +++ b/src/modules/internal.proxy/m3.global.json @@ -0,0 +1,4 @@ +{ + "name": "internal.proxy", + "dependencies": [] +} diff --git a/src/modules/internal.proxy/m3.local.json b/src/modules/internal.proxy/m3.local.json new file mode 100644 index 0000000..a0fe3f0 --- /dev/null +++ b/src/modules/internal.proxy/m3.local.json @@ -0,0 +1,3 @@ +{ + "manual": true +} diff --git a/src/module/proxy/shared.ts b/src/modules/internal.proxy/shared.ts similarity index 100% rename from src/module/proxy/shared.ts rename to src/modules/internal.proxy/shared.ts diff --git a/src/util/import-modules.ts b/src/util/import-modules.ts index 93ca0ce..fe9cc2b 100644 --- a/src/util/import-modules.ts +++ b/src/util/import-modules.ts @@ -24,6 +24,8 @@ export async function* importModulesGenerator( index: string, callbacks?: Callbacks, ): AsyncGenerator { + console.log('importModulesGenerator', { directory, index }); + for (const entry of await readdir(directory, { withFileTypes: true })) { const path = resolve(entry.path, entry.name, index); diff --git a/src/util/import.ts b/src/util/import.ts new file mode 100644 index 0000000..5529c7d --- /dev/null +++ b/src/util/import.ts @@ -0,0 +1,10 @@ +import { readdir } from 'fs/promises'; +import { resolve } from 'path'; + +export async function importDirectory(path: string): Promise { + for (const entry of await readdir(path, { withFileTypes: true })) { + if (!entry.isFile()) continue; + + await import(resolve(path, entry.name)); + } +} diff --git a/src/util/table.ts b/src/util/table.ts new file mode 100644 index 0000000..f27d081 --- /dev/null +++ b/src/util/table.ts @@ -0,0 +1,54 @@ +import stripAnsi from 'strip-ansi'; +import table from 'text-table'; + +class TableGenerator { + // #region Taken directly from table.Options + + /** Separator to use between columns, (default: ' '). */ + hsep?: string | undefined; + + /** An array of alignment types for each column, default ['l','l',...]. */ + align?: Array<'l' | 'r' | 'c' | '.' | null | undefined> | undefined; + + /** A callback function to use when calculating the string length. */ + stringLength?(str: string): number; + + // #endregion + + public generate(data: Record[] | string[][]): string { + const rows: string[][] = []; + + if (typeof data[0] === 'object') { + const keys = new Set(); + + for (const row of data) { + for (const key of Object.keys(row)) { + keys.add(key); + } + } + + const header = Array.from(keys); + + rows.push(header); + + for (const inRow of data as Record[]) { + const outRow = header.map((key) => inRow[key] ?? ''); + + rows.push(outRow); + } + } + + return table(rows, { + hsep: this.hsep, + align: this.align, + stringLength: this.stringLength, + }); + } +} + +const generator = new TableGenerator(); +generator.stringLength = (string) => stripAnsi(string).length; + +export function createTable(data: Record[] | string[][]) { + return generator.generate(data); +} diff --git a/src/worker/index.ts b/src/worker/index.ts index 1c71043..7ddf4c6 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -1,15 +1,9 @@ import chalk from 'chalk'; -import { dirname, resolve } from 'path'; -import { fileURLToPath } from 'url'; +import { resolve } from 'path'; import { importModules } from '../util/import-modules.js'; -if (!('require' in globalThis)) { - globalThis.__filename = fileURLToPath(import.meta.url); - globalThis.__dirname = dirname(__filename); -} - -const MODULE_DIR_PATH = resolve(__dirname, '../module'); +const MODULES_DIR_PATH = resolve(import.meta.dirname, '../modules'); console.group('Loading modules... (local)'); @@ -17,7 +11,7 @@ const start = performance.now(); let moduleStart = NaN; -await importModules(MODULE_DIR_PATH, 'local.js', { +await importModules(MODULES_DIR_PATH, 'local.js', { pre(entry) { const module = entry.name; console.group(`Loading ${module}...`);