@mincraft/cli 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import{Option as I,program as n}from"commander";import*as v from"readline";import{createBot as M}from"mineflayer";import{SocksClient as E}from"socks";var p=class{proxy;client;server;options;logger;isLoggedIn;bot;constructor({proxy:t,client:e,server:o,options:i},s){this.proxy=t,this.client=e,this.server=o,this.options=i,this.logger=s,this.isLoggedIn=!1}async registerAndLogIn(){let t={host:this.server.host,port:this.server.port,username:this.client.account.email,auth:this.client.account.auth,version:this.client.version,hideErrors:!0};this.proxy&&(t.connect=e=>{this.connectViaProxy(e)}),this.bot=M(t),this.options.verboseLogging&&this.registerClientEvents(),this.registerBotEvents()}connectViaProxy(t){this.logger.log("connecting via proxy");let{host:e,port:o,user:i,pass:s,type:a}=this.proxy,c=this.server;E.createConnection({proxy:{host:e,port:o,type:a,userId:i,password:s},command:"connect",destination:{host:c.host,port:c.port},timeout:3e4}).then(l=>{t.setSocket(l.socket),t.emit("connect")}).catch(l=>{this.logger.error(`proxy connection failed: ${l.message}`),t.emit("error",l)})}registerClientEvents(){this.bot._client.on("connect",()=>{this.logger.log("TCP connection established")}),this.bot._client.on("login",()=>{this.logger.log("login packet received")}),this.bot._client.on("error",t=>{this.logger.error(`client error: ${t.message}`)}),this.bot._client.on("end",t=>{this.logger.warn(`client ended: ${t}`)})}registerBotEvents(){this.bot.once("spawn",()=>{let{ign:t,uuid:e}=this.client.account;this.logger.log(`logged in as ${t} (${e})`),this.isLoggedIn=!0}),this.bot.once("kicked",t=>{this.logger.warn(`kicked: ${JSON.stringify(t)}`),this.disconnect()}),this.bot.once("error",t=>{this.logger.error(`bot error: ${t.message}`),this.isLoggedIn=!1}),this.bot.once("end",t=>{this.logger.warn(`disconnected by server: ${JSON.stringify(t)}`),this.disconnect()}),this.options.logMessages&&this.bot.on("messagestr",t=>{let e=t.split(`
3
- `);if(e.length===1)this.logger.log(t,"chat");else{this.logger.log(e[0],"chat");for(let o=1;o<e.length;o++)this.logger.raw(` ${e[o]}`)}})}disconnect(){this.bot&&(this.proxy?this.bot._client.socket?.destroy():this.bot.quit(),this.isLoggedIn=!1)}sendMessage(t){this.bot.chat(t)}};import*as u from"readline";function m(r){let t=(o,i="")=>`[mc${i?`/${i}`:""}] ${o}`,e=o=>{r?(u.clearLine(process.stdout,0),u.cursorTo(process.stdout,0),console.log(o),r.prompt(!0)):console.log(o)};return{log:(o,i)=>e(t(o,i)),warn:(o,i="warn")=>e(t(o,i)),error:(o,i="err")=>e(t(o,i)),raw:e}}import*as g from"fs/promises";import*as d from"path";import*as x from"esbuild";async function O(r){let e=(await x.build({entryPoints:[r],bundle:!0,write:!1,platform:"node",format:"esm",target:"es2022",loader:{".ts":"ts"},external:["mineflayer"],logLevel:"silent"})).outputFiles[0].text,o=r.replace(/\.ts$/,".macro.mjs");return await g.writeFile(o,e),o}async function S(r){try{await g.unlink(r)}catch{}}function F(r){return d.isAbsolute(r)?r:d.resolve(process.cwd(),r)}async function k(r){try{return await g.access(r),!0}catch{return!1}}async function b(r,t){let e=F(r),o=e,i=null;try{if(!await k(e))return t.logger.error(`macro file not found: ${e}`),!1;e.endsWith(".ts")&&(t.logger.log("compiling typescript macro"),i=await O(e),o=i);let c=await import(`file://${o}?t=${Date.now()}`);return typeof c.default!="function"?(t.logger.error("macro file must export a default function"),!1):(await c.default(t),!0)}catch(s){return t.logger.error(`could not load macro: ${s.message}`),!1}finally{i&&await S(i)}}var C=(i=>(i.Exit=".exit",i.Logout=".lo",i.Login=".li",i.Macro=".macro",i))(C||{});function w(r){return Object.values(C).some(t=>r.startsWith(t))}async function L(r){let t=null,e=v.createInterface({input:process.stdin,output:process.stdout}),o=m(e);e.setPrompt("> ");let i=async s=>{if(s=s.trim(),!w(s)&&!t?.isLoggedIn){o.raw("log in using .li before sending messages"),e.prompt();return}if(w(s)){let[a,...c]=s.split(" "),l=!1;switch(a){case".exit":{t?.disconnect(),e.close(),l=!0;break}case".li":{t?.isLoggedIn?o.error("client is already logged in"):(t=new p(r,o),t.registerAndLogIn());break}case".lo":{t?.isLoggedIn?t?.disconnect():o.error("client isn't logged in");break}case".macro":{if(!t?.isLoggedIn){o.error("client must be logged in to run macros");break}let f=c.join(" ");if(!f){o.error("usage: .macro <filepath>");break}let y=t.bot;y&&(o.log(`loading macro: ${f}`),await b(f,{bot:y,logger:o}));break}}l&&process.exit(0)}else t?.sendMessage(s);e.prompt()};e.on("line",i),console.log("=== mincraft REPL ==="),console.log("input plain text to send a message to the server"),console.log(`input .li to log in to ${r.server.host}`),console.log(`input .lo to log out from ${r.server.host}`),console.log("input .macro <filepath> to run a macro"),console.log("input .exit to exit the REPL"),e.prompt()}n.name("mincraft").description("Mineflayer CLI tool with supports for macros and proxies.").version("1.0.0").addHelpText("after",`
2
+ import{Option as I,program as l}from"commander";import*as C from"readline";import{createBot as M}from"mineflayer";import{SocksClient as O}from"socks";var d=class{proxy;client;server;options;logger;isLoggedIn;bot;constructor({proxy:t,client:e,server:i,options:r},s){this.proxy=t,this.client=e,this.server=i,this.options=r,this.logger=s,this.isLoggedIn=!1}async registerAndLogIn(){let t={host:this.server.host,port:this.server.port,username:this.client.account.email,auth:this.client.account.auth,version:this.client.version,hideErrors:!0};this.proxy&&(t.connect=e=>{this.connectViaProxy(e)}),this.bot=M(t),this.options.verboseLogging&&this.registerClientEvents(),this.registerBotEvents()}connectViaProxy(t){this.logger.log("connecting via proxy");let{host:e,port:i,user:r,pass:s,type:n}=this.proxy,a=this.server;O.createConnection({proxy:{host:e,port:i,type:n,userId:r,password:s},command:"connect",destination:{host:a.host,port:a.port},timeout:3e4}).then(c=>{t.setSocket(c.socket),t.emit("connect")}).catch(c=>{this.logger.error(`proxy connection failed: ${c.message}`),t.emit("error",c)})}registerClientEvents(){this.bot._client.on("connect",()=>{this.logger.log("TCP connection established")}),this.bot._client.on("login",()=>{this.logger.log("login packet received")}),this.bot._client.on("error",t=>{this.logger.error(`client error: ${t.message}`)}),this.bot._client.on("end",t=>{this.logger.warn(`client ended: ${t}`)})}registerBotEvents(){this.bot.once("spawn",()=>{let{ign:t,uuid:e}=this.client.account;this.logger.log(`logged in as ${t} (${e})`),this.isLoggedIn=!0}),this.bot.once("kicked",t=>{this.logger.warn(`kicked: ${JSON.stringify(t)}`),this.disconnect()}),this.bot.once("error",t=>{this.logger.error(`bot error: ${t.message}`),this.isLoggedIn=!1}),this.bot.once("end",t=>{this.logger.warn(`disconnected by server: ${JSON.stringify(t)}`),this.disconnect()}),this.options.logMessages&&this.bot.on("messagestr",t=>{let e=t.split(`
3
+ `);if(e.length===1)this.logger.log(t,"chat");else{this.logger.log(e[0],"chat");for(let i=1;i<e.length;i++)this.logger.raw(` ${e[i]}`)}})}disconnect(){this.bot&&(this.proxy?this.bot._client.socket?.destroy():this.bot.quit(),this.isLoggedIn=!1)}sendMessage(t){this.bot.chat(t)}};import*as f from"readline";var S="\x1B[90m",F="\x1B[34m",x="\x1B[0m";function D(o){return o?`/${o.split("/").map((i,r)=>r===0?`${S}${i}${x}`:`${F}${i}${x}`).join("/")}`:""}function g(o,t){let e=(r,s="")=>{let n=t?s?`${t}/${s}`:t:s;return`[mc${D(n)}] ${r}`},i=r=>{o?(f.clearLine(process.stdout,0),f.cursorTo(process.stdout,0),console.log(r),o.prompt(!0)):console.log(r)};return{log:(r,s)=>i(e(r,s)),warn:(r,s="warn")=>i(e(r,s)),error:(r,s="err")=>i(e(r,s)),raw:i}}import*as p from"fs/promises";import*as m from"path";import*as b from"esbuild";async function T(o){let e=(await b.build({entryPoints:[o],bundle:!0,write:!1,platform:"node",format:"esm",target:"es2022",loader:{". ts":"ts"},external:["mineflayer"],logLevel:"silent"})).outputFiles[0].text,i=o.replace(/\.ts$/,". macro.mjs");return await p.writeFile(i,e),i}async function k(o){try{await p.unlink(o)}catch{}}function A(o){return m.isAbsolute(o)?o:m.resolve(process.cwd(),o)}async function U(o){try{return await p.access(o),!0}catch{return!1}}function j(o){return m.basename(o).replace(/\.(ts|js|mjs)$/,"")}async function w(o,t,e){let i=A(o),r=i,s=null,n=g(e,`macro/${j(o)}`);try{if(!await U(i))return n.error(`file not found: ${i}`),!1;i.endsWith(".ts")&&(n.log("compiling macro to js"),s=await T(i),r=s);let u=await import(`file://${r}?t=${Date.now()}`);return typeof u.default!="function"?(n.error("macro file must export a default function"),!1):(n.log("running"),await u.default({bot:t,logger:n}),n.log("finished"),!0)}catch(a){return n.error(`failed: ${a.message}`),!1}finally{s&&await k(s)}}var $=(r=>(r.Exit=".exit",r.Logout=".lo",r.Login=".li",r.Macro=".macro",r))($||{});function v(o){return Object.values($).some(t=>o.startsWith(t))}async function L(o,t){let e=null,i=C.createInterface({input:process.stdin,output:process.stdout}),r=g(i);i.setPrompt("> ");let s=async n=>{if(n=n.trim(),!n)return!1;if(!v(n)&&!e?.isLoggedIn)return r.raw("log in using .li before sending messages"),!1;if(v(n)){let[a,...c]=n.split(" ");switch(a){case".exit":return e?.disconnect(),i.close(),!0;case".li":{e?.isLoggedIn?r.error("client is already logged in"):(e=new d(o,r),e.registerAndLogIn());break}case".lo":{e?.isLoggedIn?e?.disconnect():r.error("client isn't logged in");break}case".macro":{if(!e?.isLoggedIn){r.error("client must be logged in to run macros");break}let u=c.join(" ");if(!u){r.error("usage: .macro <filepath>");break}let y=e.bot;y&&await w(u,y,i);break}}}else e?.sendMessage(n);return!1};if(i.on("line",async n=>{await s(n)&&process.exit(0),i.prompt()}),console.log("=== mincraft REPL ==="),console.log("input plain text to send a message to the server"),console.log(`input .li to log in to ${o.server.host}`),console.log(`input .lo to log out from ${o.server.host}`),console.log("input .macro <filepath> to run a macro"),console.log("input .exit to exit the REPL"),t&&t.length>0)for(let{command:n,delay:a}of t)n&&(r.raw(`> ${n}`),await s(n)&&process.exit(0)),a>0&&await new Promise(c=>setTimeout(c,a));i.prompt()}l.name("mincraft").description("Mineflayer CLI tool with supports for macros and proxies.").version("1.0.0").addHelpText("after",`
4
4
  Examples:
5
5
  $ mincraft mc.hypixel.net 1.21.4 --ign FuriousDestroyer --email you@example.com
6
6
  $ mincraft mythic.gg 1.7.10 -p 58585 --ign MangoSyrup --email you@example.com --prox proxy.com:1234:mango:secret
7
- `);n.argument("<host>","Server hostname").option("-p, --port <PORT>","Server port",r=>parseInt(r,10),25565);n.argument("<version>","Client version, e.g. 1.21.4");var P=["host","port","user","pass"],$=":";n.option("--prox <HOST:PORT:USER:PASS>","Connect to the server with a proxy").option("--prox-field-order <FORMAT>",'The order of the proxy credential fields, e.g. "user,pass,host,port".').option("--prox-field-sep <SEP>","The separator of the proxy credential fields").option("--prox-type <4|5>","The SOCKS proxy type",r=>parseInt(r,10),5);var T="microsoft";n.addOption(new I("--ign <IN-GAME NAME>","Player username").conflicts("uuid")).addOption(new I("--uuid <UUID>","Player UUID").conflicts("ign")).requiredOption("--email <EMAIL>","Account email").option("--auth <AUTH>","Account authentication method",T);n.option("--no-log-messages","Do not log messages your client receives").option("--verbose","Enable additional logging messages");n.action(async(r,t,e)=>{if(!e.ign&&!e.uuid&&n.error("error: must specify either --ign or --uuid"),e.proxFieldOrder){let i=e.proxyFieldSep??$;e.proxFieldOrder.split(i).every(a=>P.includes(a))||n.error("error: --prox-field-order includes invalid fields")}let o=await B(n);await L(o)});async function A(r){let e=await(await fetch(`https://sessionserver.mojang.com/session/minecraft/profile/${r}`)).json();if(!e?.name)throw new Error("Could not fetch IGN from UUID");return e.name}async function U(r){let e=await(await fetch(`https://api.mojang.com/users/profiles/minecraft/${r}`)).json();if(!e?.id)throw new Error("Could not fetch UUID from IGN");return e.id}function j(r,t,e){let o=r.split(e),i=Object.fromEntries(t.map((s,a)=>[s,o[a]]));return i.port=+i.port,i}async function B(r){let t=r.args,e=r.opts();return{server:{host:t[0],port:e.port},client:{version:t[1],account:{auth:e.auth,uuid:e.uuid??await U(e.ign),ign:e.ign??await A(e.uuid),email:e.email}},...e.prox?{proxy:{...j(e.prox,e.proxFieldOrder??P,e.proxFieldSep??$),type:e.proxType}}:{},options:{logMessages:e.noLogMessages??!0,verboseLogging:e.verbose??!1}}}var h=m(),Y=h.log,Z=h.warn,ee=h.error;n.parse();export{h as defaultLogger,ee as error,Y as log,Z as warn};
7
+ $ mincraft mc.hypixel.net 1.21.4 --ign Player --email you@example.com --exec "{{1000}}.li{{2000}}\\nhello{{500}}\\n.lo"
8
+ `);l.argument("<host>","Server hostname").option("-p, --port <PORT>","Server port",o=>parseInt(o,10),25565);l.argument("<version>","Client version, e.g. 1.21.4");var E=["host","port","user","pass"],P=":";l.option("--prox <HOST:PORT:USER:PASS>","Connect to the server with a proxy").option("--prox-field-order <FORMAT>",'The order of the proxy credential fields, e.g. "user,pass,host,port".').option("--prox-field-sep <SEP>","The separator of the proxy credential fields").option("--prox-type <4|5>","The SOCKS proxy type",o=>parseInt(o,10),5);var B="microsoft";l.addOption(new I("--ign <IN-GAME NAME>","Player username").conflicts("uuid")).addOption(new I("--uuid <UUID>","Player UUID").conflicts("ign")).requiredOption("--email <EMAIL>","Account email").option("--auth <AUTH>","Account authentication method",B);l.option("--no-log-messages","Do not log messages your client receives").option("--verbose","Enable additional logging messages").option("--exec <COMMANDS>",'Execute REPL commands on startup with optional millisecond-based delays, e.g. "{{1000}}.li{{2000}}\\nhello{{500}}\\n.lo".',(o,t)=>t.concat(o),[]);l.action(async(o,t,e)=>{if(!e.ign&&!e.uuid&&l.error("error: must specify either --ign or --uuid"),e.proxFieldOrder){let s=e.proxyFieldSep??P;e.proxFieldOrder.split(s).every(a=>E.includes(a))||l.error("error: --prox-field-order includes invalid fields")}let i=await H(l),r=[];for(let s of e.exec){let n=N(s);r.push(...n)}await L(i,r.length>0?r:void 0)});function N(o){let t=[],e=o.split("\\n").filter(Boolean);for(let i of e){let r=i.match(/^\{\{(\d+)\}\}/);if(r){let n=parseInt(r[1],10);t.push({command:"",delay:n})}let s=i.replace(/^\{\{\d+\}\}/,"");if(s){let n=s.match(/\{\{(\d+)\}\}$/);if(n){let a=parseInt(n[1],10),c=s.replace(/\{\{\d+\}\}$/,"");t.push({command:c,delay:a})}else t.push({command:s,delay:0})}}return t}async function R(o){let e=await(await fetch(`https://sessionserver.mojang.com/session/minecraft/profile/${o}`)).json();if(!e?.name)throw new Error("Could not fetch IGN from UUID");return e.name}async function _(o){let e=await(await fetch(`https://api.mojang.com/users/profiles/minecraft/${o}`)).json();if(!e?.id)throw new Error("Could not fetch UUID from IGN");return e.id}function G(o,t,e){let i=o.split(e),r=Object.fromEntries(t.map((s,n)=>[s,i[n]]));return r.port=+r.port,r}async function H(o){let t=o.args,e=o.opts();return{server:{host:t[0],port:e.port},client:{version:t[1],account:{auth:e.auth,uuid:e.uuid??await _(e.ign),ign:e.ign??await R(e.uuid),email:e.email}},...e.prox?{proxy:{...G(e.prox,e.proxFieldOrder??E,e.proxFieldSep??P),type:e.proxType}}:{},options:{logMessages:e.noLogMessages??!0,verboseLogging:e.verbose??!1}}}var h=g(),ne=h.log,se=h.warn,ae=h.error;l.parse();export{h as defaultLogger,ae as error,ne as log,se as warn};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mincraft/cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Mineflayer CLI tool with support for macros and proxies",
5
5
  "bin": {
6
6
  "mincraft": "./dist/index.mjs"
@@ -17,7 +17,7 @@
17
17
  "esbuild": "^0.27.2",
18
18
  "mineflayer": "^4.33.0",
19
19
  "socks": "^2.8.7",
20
- "@mincraft/types": "1.2.0"
20
+ "@mincraft/types": "1.3.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/debug": "^4.1.12",
package/src/cli.ts CHANGED
@@ -1,6 +1,10 @@
1
- import type { Config, ProxyCredentials } from "@mincraft/types";
1
+ import type {
2
+ Config,
3
+ DelayableCommands,
4
+ ProxyCredentials,
5
+ } from "@mincraft/types";
2
6
  import { type Command, Option, program } from "commander";
3
- import { run } from "./runner";
7
+ import { run } from "./repl";
4
8
 
5
9
  // meta
6
10
  program
@@ -13,6 +17,7 @@ program
13
17
  Examples:
14
18
  $ mincraft mc.hypixel.net 1.21.4 --ign FuriousDestroyer --email you@example.com
15
19
  $ mincraft mythic.gg 1.7.10 -p 58585 --ign MangoSyrup --email you@example.com --prox proxy.com:1234:mango:secret
20
+ $ mincraft mc.hypixel.net 1.21.4 --ign Player --email you@example.com --exec "{{1000}}.li{{2000}}\\nhello{{500}}\\n.lo"
16
21
  `,
17
22
  );
18
23
 
@@ -66,7 +71,13 @@ program
66
71
  // extra options
67
72
  program
68
73
  .option("--no-log-messages", "Do not log messages your client receives")
69
- .option("--verbose", "Enable additional logging messages");
74
+ .option("--verbose", "Enable additional logging messages")
75
+ .option(
76
+ "--exec <COMMANDS>",
77
+ 'Execute REPL commands on startup with optional millisecond-based delays, e.g. "{{1000}}.li{{2000}}\\nhello{{500}}\\n.lo".',
78
+ (val: string, prev: string[]) => prev.concat(val),
79
+ [],
80
+ );
70
81
 
71
82
  program.action(async (_host, _version, options) => {
72
83
  if (!options.ign && !options.uuid) {
@@ -83,9 +94,42 @@ program.action(async (_host, _version, options) => {
83
94
  }
84
95
 
85
96
  const config = await getConfig(program);
86
- await run(config);
97
+
98
+ const commands: DelayableCommands = [];
99
+ for (const cmd of options.exec) {
100
+ const parsed = parseExecCommands(cmd);
101
+ commands.push(...parsed);
102
+ }
103
+ await run(config, commands.length > 0 ? commands : undefined);
87
104
  });
88
105
 
106
+ function parseExecCommands(input: string) {
107
+ const result: DelayableCommands = [];
108
+ const lines = input.split("\\n").filter(Boolean);
109
+
110
+ for (const line of lines) {
111
+ const initialDelayMatch = line.match(/^\{\{(\d+)\}\}/);
112
+ if (initialDelayMatch) {
113
+ const initialDelay = parseInt(initialDelayMatch[1], 10);
114
+ result.push({ command: "", delay: initialDelay });
115
+ }
116
+
117
+ const lineWithoutInitial = line.replace(/^\{\{\d+\}\}/, "");
118
+ if (lineWithoutInitial) {
119
+ const trailingDelayMatch = lineWithoutInitial.match(/\{\{(\d+)\}\}$/);
120
+ if (trailingDelayMatch) {
121
+ const delay = parseInt(trailingDelayMatch[1], 10);
122
+ const command = lineWithoutInitial.replace(/\{\{\d+\}\}$/, "");
123
+ result.push({ command, delay });
124
+ } else {
125
+ result.push({ command: lineWithoutInitial, delay: 0 });
126
+ }
127
+ }
128
+ }
129
+
130
+ return result;
131
+ }
132
+
89
133
  async function getIGN(uuid: string) {
90
134
  const res = await fetch(
91
135
  `https://sessionserver.mojang.com/session/minecraft/profile/${uuid}`,
package/src/logger.ts CHANGED
@@ -1,9 +1,34 @@
1
1
  import * as readline from "node:readline";
2
2
  import type { Logger } from "@mincraft/types";
3
3
 
4
- export function createLogger(rl?: readline.Interface): Logger {
5
- const format = (msg: string, scope = "") =>
6
- `[mc${scope ? `/${scope}` : ""}] ${msg}`;
4
+ const GRAY = "\x1b[90m";
5
+ const BLUE = "\x1b[34m";
6
+ const RESET = "\x1b[0m";
7
+
8
+ function formatScope(fullScope: string) {
9
+ if (!fullScope) return "";
10
+
11
+ const parts = fullScope.split("/");
12
+ const colored = parts.map((part, i) => {
13
+ if (i === 0) return `${GRAY}${part}${RESET}`;
14
+ return `${BLUE}${part}${RESET}`;
15
+ });
16
+
17
+ return `/${colored.join("/")}`;
18
+ }
19
+
20
+ export function createLogger(
21
+ rl?: readline.Interface,
22
+ baseScope?: string,
23
+ ): Logger {
24
+ const format = (msg: string, scope = "") => {
25
+ const fullScope = baseScope
26
+ ? scope
27
+ ? `${baseScope}/${scope}`
28
+ : baseScope
29
+ : scope;
30
+ return `[mc${formatScope(fullScope)}] ${msg}`;
31
+ };
7
32
 
8
33
  const write = (msg: string) => {
9
34
  if (rl) {
package/src/macro.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import type { MacroContext, MacroModule } from "@mincraft/types";
3
+ import type { Interface } from "node:readline";
4
+ import type { MacroModule } from "@mincraft/types";
4
5
  import * as esbuild from "esbuild";
6
+ import type { Bot } from "mineflayer";
7
+ import { createLogger } from "./logger";
5
8
 
6
9
  async function compileToJS(filepath: string) {
7
10
  const result = await esbuild.build({
@@ -11,13 +14,13 @@ async function compileToJS(filepath: string) {
11
14
  platform: "node",
12
15
  format: "esm",
13
16
  target: "es2022",
14
- loader: { ".ts": "ts" },
17
+ loader: { ". ts": "ts" },
15
18
  external: ["mineflayer"],
16
19
  logLevel: "silent",
17
20
  });
18
21
 
19
22
  const jsCode = result.outputFiles[0].text;
20
- const jsFilePath = filepath.replace(/\.ts$/, ".macro.mjs");
23
+ const jsFilePath = filepath.replace(/\.ts$/, ". macro.mjs");
21
24
  await fs.writeFile(jsFilePath, jsCode);
22
25
 
23
26
  return jsFilePath;
@@ -45,21 +48,31 @@ async function fileExists(filepath: string) {
45
48
  }
46
49
  }
47
50
 
48
- export async function loadAndRunMacro(filepath: string, ctx: MacroContext) {
51
+ function getMacroName(filepath: string) {
52
+ return path.basename(filepath).replace(/\.(ts|js|mjs)$/, "");
53
+ }
54
+
55
+ export async function loadAndRunMacro(
56
+ filepath: string,
57
+ bot: Bot,
58
+ rl?: Interface,
59
+ ) {
49
60
  const absolutePath = resolveMacroPath(filepath);
50
61
  let importPath = absolutePath;
51
62
  let compiledPath: string = null;
52
63
 
64
+ const logger = createLogger(rl, `macro/${getMacroName(filepath)}`);
65
+
53
66
  try {
54
67
  if (!(await fileExists(absolutePath))) {
55
- ctx.logger.error(`macro file not found: ${absolutePath}`);
68
+ logger.error(`file not found: ${absolutePath}`);
56
69
  return false;
57
70
  }
58
71
 
59
72
  const isTypescriptFile = absolutePath.endsWith(".ts");
60
73
 
61
74
  if (isTypescriptFile) {
62
- ctx.logger.log("compiling typescript macro");
75
+ logger.log("compiling macro to js");
63
76
  compiledPath = await compileToJS(absolutePath);
64
77
  importPath = compiledPath;
65
78
  }
@@ -68,14 +81,16 @@ export async function loadAndRunMacro(filepath: string, ctx: MacroContext) {
68
81
  const module = (await import(fileUrl)) as MacroModule;
69
82
 
70
83
  if (typeof module.default !== "function") {
71
- ctx.logger.error("macro file must export a default function");
84
+ logger.error("macro file must export a default function");
72
85
  return false;
73
86
  }
74
87
 
75
- await module.default(ctx);
88
+ logger.log("running");
89
+ await module.default({ bot, logger });
90
+ logger.log("finished");
76
91
  return true;
77
92
  } catch (err) {
78
- ctx.logger.error(`could not load macro: ${(err as Error).message}`);
93
+ logger.error(`failed: ${(err as Error).message}`);
79
94
  return false;
80
95
  } finally {
81
96
  if (compiledPath) {
@@ -1,5 +1,5 @@
1
1
  import * as readline from "node:readline";
2
- import type { Config } from "@mincraft/types";
2
+ import type { Config, DelayableCommands } from "@mincraft/types";
3
3
  import { BotClient } from "./bot";
4
4
  import { createLogger } from "./logger";
5
5
  import { loadAndRunMacro } from "./macro";
@@ -15,8 +15,8 @@ function isCommand(value: string) {
15
15
  return Object.values(BotCommand).some((cmd) => value.startsWith(cmd));
16
16
  }
17
17
 
18
- export async function run(config: Config) {
19
- let bot: BotClient | null = null;
18
+ export async function run(config: Config, commands?: DelayableCommands) {
19
+ let bot: BotClient = null;
20
20
 
21
21
  const rl = readline.createInterface({
22
22
  input: process.stdin,
@@ -30,22 +30,23 @@ export async function run(config: Config) {
30
30
  const handleInput = async (input: string) => {
31
31
  input = input.trim();
32
32
 
33
+ if (!input) {
34
+ return false;
35
+ }
36
+
33
37
  if (!isCommand(input) && !bot?.isLoggedIn) {
34
38
  logger.raw("log in using .li before sending messages");
35
- rl.prompt();
36
- return;
39
+ return false;
37
40
  }
38
41
 
39
42
  if (isCommand(input)) {
40
43
  const [cmd, ...args] = input.split(" ");
41
- let exit = false;
42
44
 
43
45
  switch (cmd) {
44
46
  case BotCommand.Exit: {
45
47
  bot?.disconnect();
46
48
  rl.close();
47
- exit = true;
48
- break;
49
+ return true;
49
50
  }
50
51
  case BotCommand.Login: {
51
52
  if (bot?.isLoggedIn) {
@@ -76,24 +77,25 @@ export async function run(config: Config) {
76
77
  }
77
78
  const mineflayer = bot.bot;
78
79
  if (mineflayer) {
79
- logger.log(`loading macro: ${filepath}`);
80
- await loadAndRunMacro(filepath, { bot: mineflayer, logger });
80
+ await loadAndRunMacro(filepath, mineflayer, rl);
81
81
  }
82
82
  break;
83
83
  }
84
84
  }
85
-
86
- if (exit) {
87
- process.exit(0);
88
- }
89
85
  } else {
90
86
  bot?.sendMessage(input);
91
87
  }
92
88
 
93
- rl.prompt();
89
+ return false;
94
90
  };
95
91
 
96
- rl.on("line", handleInput);
92
+ rl.on("line", async (input) => {
93
+ const shouldExit = await handleInput(input);
94
+ if (shouldExit) {
95
+ process.exit(0);
96
+ }
97
+ rl.prompt();
98
+ });
97
99
 
98
100
  console.log("=== mincraft REPL ===");
99
101
  console.log("input plain text to send a message to the server");
@@ -102,5 +104,20 @@ export async function run(config: Config) {
102
104
  console.log("input .macro <filepath> to run a macro");
103
105
  console.log("input .exit to exit the REPL");
104
106
 
107
+ if (commands && commands.length > 0) {
108
+ for (const { command, delay } of commands) {
109
+ if (command) {
110
+ logger.raw(`> ${command}`);
111
+ const shouldExit = await handleInput(command);
112
+ if (shouldExit) {
113
+ process.exit(0);
114
+ }
115
+ }
116
+ if (delay > 0) {
117
+ await new Promise((resolve) => setTimeout(resolve, delay));
118
+ }
119
+ }
120
+ }
121
+
105
122
  rl.prompt();
106
123
  }