@powernukkitx/cli 0.0.3 → 1.0.1
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/LICENSE +21 -0
- package/README.md +110 -101
- package/dist/cli.js +34 -0
- package/dist/commands/backup.d.ts +2 -1
- package/dist/commands/backup.js +23 -74
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +60 -114
- package/dist/commands/console.d.ts +2 -0
- package/dist/commands/console.js +98 -0
- package/dist/commands/doctor.d.ts +2 -1
- package/dist/commands/doctor.js +97 -119
- package/dist/commands/info.d.ts +2 -1
- package/dist/commands/info.js +41 -36
- package/dist/commands/init.d.ts +2 -4
- package/dist/commands/init.js +87 -73
- package/dist/commands/install.js +45 -0
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +60 -0
- package/dist/commands/menu.js +94 -0
- package/dist/commands/plugin/index.d.ts +2 -0
- package/dist/commands/plugin/index.js +13 -0
- package/dist/commands/plugin/info.d.ts +2 -0
- package/dist/commands/plugin/info.js +39 -0
- package/dist/commands/plugin/install.d.ts +2 -0
- package/dist/commands/plugin/install.js +56 -0
- package/dist/commands/plugin/installed.d.ts +2 -0
- package/dist/commands/plugin/installed.js +41 -0
- package/dist/commands/plugin/list.d.ts +2 -0
- package/dist/commands/plugin/list.js +47 -0
- package/dist/commands/plugin/remove.d.ts +2 -0
- package/dist/commands/plugin/remove.js +44 -0
- package/dist/commands/plugin/search.d.ts +2 -0
- package/dist/commands/plugin/search.js +11 -0
- package/dist/commands/plugin/update.d.ts +2 -0
- package/dist/commands/plugin/update.js +52 -0
- package/dist/commands/start.d.ts +2 -7
- package/dist/commands/start.js +74 -108
- package/dist/commands/stop.d.ts +2 -0
- package/dist/commands/stop.js +26 -0
- package/dist/commands/update.d.ts +2 -5
- package/dist/commands/update.js +34 -55
- package/dist/commands/use.d.ts +2 -0
- package/dist/commands/use.js +39 -0
- package/dist/infra/http.js +119 -0
- package/dist/infra/paths.js +15 -0
- package/dist/infra/store.js +24 -0
- package/dist/services/backup.js +34 -0
- package/dist/services/plugins.js +139 -0
- package/dist/services/process.js +41 -0
- package/dist/services/release.js +51 -0
- package/dist/services/server.js +81 -0
- package/dist/ui/output.js +67 -0
- package/dist/ui/prompts.js +26 -0
- package/dist/ui/theme.js +16 -0
- package/package.json +44 -63
- package/dist/bundle.js +0 -18871
- package/dist/commands/plugin.d.ts +0 -12
- package/dist/commands/plugin.js +0 -385
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -260
- package/dist/lang/en.json +0 -178
- package/dist/lang/fr.json +0 -178
- package/dist/lang/index.d.ts +0 -7
- package/dist/lang/index.js +0 -83
- package/dist/plugins.json +0 -66
- package/dist/types.d.ts +0 -61
- package/dist/types.js +0 -1
- package/dist/utils/api.d.ts +0 -18
- package/dist/utils/api.js +0 -74
- package/dist/utils/github.d.ts +0 -27
- package/dist/utils/github.js +0 -187
- package/dist/utils/logger.d.ts +0 -15
- package/dist/utils/logger.js +0 -72
- package/dist/utils/pause.d.ts +0 -1
- package/dist/utils/pause.js +0 -6
- package/dist/utils/plugins.d.ts +0 -4
- package/dist/utils/plugins.js +0 -34
- package/dist/utils/server.d.ts +0 -3
- package/dist/utils/server.js +0 -39
- package/dist/utils/updater.d.ts +0 -1
- package/dist/utils/updater.js +0 -86
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { createConnection } from 'node:net';
|
|
5
|
+
import { defineCommand } from 'citty';
|
|
6
|
+
import { intro, outro, log } from '../ui/output.js';
|
|
7
|
+
import { resolveServerDir } from '../services/server.js';
|
|
8
|
+
function parseServerProperties(serverPath) {
|
|
9
|
+
const propsPath = join(serverPath, 'server.properties');
|
|
10
|
+
if (!existsSync(propsPath))
|
|
11
|
+
return {};
|
|
12
|
+
const props = {};
|
|
13
|
+
for (const line of readFileSync(propsPath, 'utf-8').split('\n')) {
|
|
14
|
+
if (line.startsWith('#') || !line.includes('='))
|
|
15
|
+
continue;
|
|
16
|
+
const [k, ...v] = line.split('=');
|
|
17
|
+
props[k.trim()] = v.join('=').trim();
|
|
18
|
+
}
|
|
19
|
+
return props;
|
|
20
|
+
}
|
|
21
|
+
function rconPacket(id, type, body) {
|
|
22
|
+
const bodyBuf = Buffer.from(body + '\0\0', 'utf-8');
|
|
23
|
+
const packet = Buffer.alloc(12 + bodyBuf.length);
|
|
24
|
+
packet.writeInt32LE(8 + bodyBuf.length, 0);
|
|
25
|
+
packet.writeInt32LE(id, 4);
|
|
26
|
+
packet.writeInt32LE(type, 8);
|
|
27
|
+
bodyBuf.copy(packet, 12);
|
|
28
|
+
return packet;
|
|
29
|
+
}
|
|
30
|
+
function connectRcon(host, port, password) {
|
|
31
|
+
return new Promise(resolve => {
|
|
32
|
+
const socket = createConnection({ host, port }, () => socket.write(rconPacket(1, 3, password)));
|
|
33
|
+
socket.once('data', data => {
|
|
34
|
+
if (data.readInt32LE(4) === -1) {
|
|
35
|
+
socket.destroy();
|
|
36
|
+
resolve(null);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
resolve({
|
|
40
|
+
send: (cmd) => {
|
|
41
|
+
socket.write(rconPacket(2, 2, cmd));
|
|
42
|
+
socket.once('data', d => {
|
|
43
|
+
const resp = d.slice(12, d.length - 2).toString('utf-8');
|
|
44
|
+
if (resp)
|
|
45
|
+
process.stdout.write(resp + '\n');
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
close: () => socket.destroy(),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
socket.on('error', () => resolve(null));
|
|
52
|
+
setTimeout(() => resolve(null), 3000);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export const consoleCmd = defineCommand({
|
|
56
|
+
meta: { description: 'Attach to server console via RCON' },
|
|
57
|
+
args: {},
|
|
58
|
+
async run() {
|
|
59
|
+
intro(`💻 ${"Server console"}`);
|
|
60
|
+
const serverPath = await resolveServerDir();
|
|
61
|
+
if (!serverPath) {
|
|
62
|
+
log.error("No server detected");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
const props = parseServerProperties(serverPath);
|
|
66
|
+
const enabled = props['enable-rcon'] === 'true';
|
|
67
|
+
const port = parseInt(props['rcon.port'] ?? '25575');
|
|
68
|
+
const password = props['rcon.password'] ?? '';
|
|
69
|
+
if (enabled && password) {
|
|
70
|
+
log.step("Connecting via RCON...");
|
|
71
|
+
const rcon = await connectRcon('127.0.0.1', port, password);
|
|
72
|
+
if (rcon) {
|
|
73
|
+
log.success("Connected — type commands (exit to quit)");
|
|
74
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: "> " });
|
|
75
|
+
rl.prompt();
|
|
76
|
+
rl.on('line', line => {
|
|
77
|
+
const cmd = line.trim();
|
|
78
|
+
if (cmd === 'exit') {
|
|
79
|
+
rcon.close();
|
|
80
|
+
rl.close();
|
|
81
|
+
outro("Disconnected");
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
rcon.send(cmd);
|
|
85
|
+
rl.prompt();
|
|
86
|
+
});
|
|
87
|
+
rl.on('close', () => { rcon.close(); process.exit(0); });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
log.warn("RCON disabled in server.properties");
|
|
92
|
+
log.info("Enable enable-rcon=true in server.properties");
|
|
93
|
+
log.dim('enable-rcon=true');
|
|
94
|
+
log.dim('rcon.port=25575');
|
|
95
|
+
log.dim('rcon.password=your_password');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import type { CommandDef } from 'citty';
|
|
2
|
+
export declare const doctorCmd: CommandDef;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,119 +1,97 @@
|
|
|
1
|
-
import { existsSync, statSync, readdirSync } from 'node:fs';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
step(t('doctor.checkingWorlds'));
|
|
99
|
-
const worldsDir = join(serverPath, 'worlds');
|
|
100
|
-
if (existsSync(worldsDir)) {
|
|
101
|
-
const worlds = readdirSync(worldsDir).filter(f => statSync(join(worldsDir, f)).isDirectory());
|
|
102
|
-
info(t('doctor.worldsFound', String(worlds.length)));
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
info(t('doctor.worldsMissing'));
|
|
106
|
-
}
|
|
107
|
-
// Port check
|
|
108
|
-
step(t('doctor.checkingPort'));
|
|
109
|
-
const portFree = await checkPort(19132);
|
|
110
|
-
if (portFree) {
|
|
111
|
-
success(t('doctor.portFree'));
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
warn(t('doctor.portInUse', '19132'));
|
|
115
|
-
}
|
|
116
|
-
divider();
|
|
117
|
-
success(t('doctor.done'));
|
|
118
|
-
await pauseForExit();
|
|
119
|
-
}
|
|
1
|
+
import { existsSync, statSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { statfs } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { createSocket } from 'node:dgram';
|
|
6
|
+
import { freemem, totalmem } from 'node:os';
|
|
7
|
+
import { defineCommand } from 'citty';
|
|
8
|
+
import { intro, outro, log, section, spin, isJson, json } from '../ui/output.js';
|
|
9
|
+
import { resolveServerDir, checkJava } from '../services/server.js';
|
|
10
|
+
import { JAR_NAME } from '../infra/paths.js';
|
|
11
|
+
function checkPort(port) {
|
|
12
|
+
return new Promise(res => {
|
|
13
|
+
const s = createSocket('udp4');
|
|
14
|
+
s.once('error', () => { s.close(); res(false); });
|
|
15
|
+
s.bind(port, '0.0.0.0', () => { s.close(); res(true); });
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function pnxVersion(jarPath) {
|
|
19
|
+
try {
|
|
20
|
+
const r = spawnSync('java', ['-jar', jarPath, '--version'], { timeout: 3000, encoding: 'utf8' });
|
|
21
|
+
return ((r.stdout || '') + (r.stderr || '')).match(/(\d+\.\d+\.\d+[\w.-]*)/)?.[1] ?? null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function diskFreeMb(dir) {
|
|
28
|
+
try {
|
|
29
|
+
const s = await statfs(dir);
|
|
30
|
+
return Math.round((s.bfree * s.bsize) / 1024 / 1024);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export const doctorCmd = defineCommand({
|
|
37
|
+
meta: { description: 'Diagnose server health' },
|
|
38
|
+
args: {},
|
|
39
|
+
async run() {
|
|
40
|
+
const sp = spin("Running diagnostics...");
|
|
41
|
+
const java = await checkJava();
|
|
42
|
+
const serverPath = await resolveServerDir();
|
|
43
|
+
const jarPath = serverPath ? join(serverPath, JAR_NAME) : null;
|
|
44
|
+
const jarExists = jarPath ? existsSync(jarPath) : false;
|
|
45
|
+
const version = jarPath && jarExists ? pnxVersion(jarPath) : null;
|
|
46
|
+
const pluginsDir = serverPath ? join(serverPath, 'plugins') : null;
|
|
47
|
+
const worldsDir = serverPath ? join(serverPath, 'worlds') : null;
|
|
48
|
+
const pluginCount = pluginsDir && existsSync(pluginsDir)
|
|
49
|
+
? readdirSync(pluginsDir).filter(f => f.endsWith('.jar')).length : 0;
|
|
50
|
+
const worldCount = worldsDir && existsSync(worldsDir)
|
|
51
|
+
? readdirSync(worldsDir).filter(f => statSync(join(worldsDir, f)).isDirectory()).length : 0;
|
|
52
|
+
const freeMb = Math.round(freemem() / 1024 / 1024);
|
|
53
|
+
const totalMb = Math.round(totalmem() / 1024 / 1024);
|
|
54
|
+
const disk = serverPath ? await diskFreeMb(serverPath) : null;
|
|
55
|
+
const portFree = await checkPort(19132);
|
|
56
|
+
sp.stop("Diagnostics complete");
|
|
57
|
+
if (isJson())
|
|
58
|
+
json({
|
|
59
|
+
java: { ok: java.ok, version: java.raw, vendor: java.vendor },
|
|
60
|
+
server: { found: !!serverPath, path: serverPath, pnxVersion: version },
|
|
61
|
+
ram: { freeMb, totalMb },
|
|
62
|
+
disk: disk !== null ? { freeMb: disk } : null,
|
|
63
|
+
port: { free: portFree, port: 19132 },
|
|
64
|
+
plugins: { count: pluginCount },
|
|
65
|
+
worlds: { count: worldCount },
|
|
66
|
+
});
|
|
67
|
+
intro(`🩺 ${"Server diagnostics"}`);
|
|
68
|
+
section("Java");
|
|
69
|
+
if (java.ok)
|
|
70
|
+
log.success(`${java.raw} (${java.vendor})`);
|
|
71
|
+
else {
|
|
72
|
+
log.error("Java 21+ required");
|
|
73
|
+
log.info('https://adoptium.net/');
|
|
74
|
+
}
|
|
75
|
+
section("Server");
|
|
76
|
+
if (serverPath) {
|
|
77
|
+
log.success(serverPath);
|
|
78
|
+
if (jarExists)
|
|
79
|
+
log.success(`${JAR_NAME}${version ? ` v${version}` : ''}`);
|
|
80
|
+
else
|
|
81
|
+
log.error("powernukkitx.jar missing");
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
log.error("No server — run 'pnx init'");
|
|
85
|
+
section("Plugins & Worlds");
|
|
86
|
+
log.info(`Plugins: ${pluginCount} | Worlds: ${worldCount}`);
|
|
87
|
+
section("System");
|
|
88
|
+
log.info(`RAM: ${freeMb} MB / ${totalMb} MB`);
|
|
89
|
+
if (disk !== null)
|
|
90
|
+
log.info(`Disk: ${disk} MB`);
|
|
91
|
+
if (portFree)
|
|
92
|
+
log.success(`Port ${'19132'} available`);
|
|
93
|
+
else
|
|
94
|
+
log.warn(`Port ${'19132'} busy`);
|
|
95
|
+
outro("Diagnostics complete");
|
|
96
|
+
},
|
|
97
|
+
});
|
package/dist/commands/info.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import type { CommandDef } from 'citty';
|
|
2
|
+
export declare const infoCmd: CommandDef;
|
package/dist/commands/info.js
CHANGED
|
@@ -1,36 +1,41 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readdir } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { freemem, totalmem } from 'node:os';
|
|
5
|
+
import { defineCommand } from 'citty';
|
|
6
|
+
import { intro, outro, log, section, isJson, json } from '../ui/output.js';
|
|
7
|
+
import { c } from '../ui/theme.js';
|
|
8
|
+
import { resolveServerDir } from '../services/server.js';
|
|
9
|
+
export const infoCmd = defineCommand({
|
|
10
|
+
meta: { description: 'Show server information' },
|
|
11
|
+
args: {},
|
|
12
|
+
async run() {
|
|
13
|
+
const serverPath = await resolveServerDir();
|
|
14
|
+
const installed = serverPath !== null;
|
|
15
|
+
let plugins = [], worlds = [];
|
|
16
|
+
if (serverPath && existsSync(join(serverPath, 'plugins'))) {
|
|
17
|
+
plugins = (await readdir(join(serverPath, 'plugins'))).filter(f => f.endsWith('.jar'));
|
|
18
|
+
}
|
|
19
|
+
if (serverPath && existsSync(join(serverPath, 'worlds'))) {
|
|
20
|
+
worlds = await readdir(join(serverPath, 'worlds'));
|
|
21
|
+
}
|
|
22
|
+
const freeMb = Math.round(freemem() / 1024 / 1024);
|
|
23
|
+
const totalMb = Math.round(totalmem() / 1024 / 1024);
|
|
24
|
+
if (isJson())
|
|
25
|
+
json({
|
|
26
|
+
status: installed ? 'installed' : 'not_installed',
|
|
27
|
+
serverDir: serverPath, plugins: plugins.length, worlds: worlds.length,
|
|
28
|
+
ram: { freeMb, totalMb },
|
|
29
|
+
});
|
|
30
|
+
intro(`📊 ${"Server info"}`);
|
|
31
|
+
section("Server");
|
|
32
|
+
log.info(`${"Status".padEnd(12)} ${installed ? c.green('✔ ' + "Installed") : c.red('✘ ' + "Not installed")}`);
|
|
33
|
+
if (serverPath)
|
|
34
|
+
log.info(`${"Path".padEnd(12)} ${c.accent(serverPath)}`);
|
|
35
|
+
log.info(`${"Plugins".padEnd(12)} ${plugins.length}`);
|
|
36
|
+
log.info(`${"Worlds".padEnd(12)} ${worlds.length}`);
|
|
37
|
+
section("System");
|
|
38
|
+
log.info(`${"Free RAM".padEnd(12)} ${freeMb} MB / ${totalMb} MB`);
|
|
39
|
+
outro(installed ? "Ready to start with 'pnx start'" : "Run 'pnx init' to create a server");
|
|
40
|
+
},
|
|
41
|
+
});
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -1,73 +1,87 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { mkdir } from 'node:fs/promises';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import { cwd } from 'node:process';
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
await
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { cwd } from 'node:process';
|
|
5
|
+
import { defineCommand } from 'citty';
|
|
6
|
+
import { intro, outro, log, spin, progress, isJson } from '../ui/output.js';
|
|
7
|
+
import { promptText, promptConfirm, promptSelect } from '../ui/prompts.js';
|
|
8
|
+
import { listReleases, downloadCore } from '../services/release.js';
|
|
9
|
+
import { startScripts, setDefaultServer } from '../services/server.js';
|
|
10
|
+
import { downloadFile } from '../infra/http.js';
|
|
11
|
+
import { JAR_NAME } from '../infra/paths.js';
|
|
12
|
+
export const initCmd = defineCommand({
|
|
13
|
+
meta: { description: 'Create a new PowerNukkitX server' },
|
|
14
|
+
args: {
|
|
15
|
+
dir: { type: 'string', description: 'Target directory' },
|
|
16
|
+
version: { type: 'string', description: 'Version to install (tag), skips the picker' },
|
|
17
|
+
dev: { type: 'boolean', description: 'Install dev version', default: false },
|
|
18
|
+
},
|
|
19
|
+
async run({ args }) {
|
|
20
|
+
intro(`⚡ ${"Create a PowerNukkitX server"}`);
|
|
21
|
+
let target = args.dir;
|
|
22
|
+
if (!target) {
|
|
23
|
+
target = await promptText({
|
|
24
|
+
message: "Server directory?",
|
|
25
|
+
defaultValue: 'pnx-server',
|
|
26
|
+
validate: v => {
|
|
27
|
+
if (!v || !v.trim())
|
|
28
|
+
return 'Required';
|
|
29
|
+
if (existsSync(join(resolve(cwd(), v), JAR_NAME)))
|
|
30
|
+
return "A server already exists here";
|
|
31
|
+
return undefined;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const serverDir = resolve(cwd(), target);
|
|
36
|
+
if (existsSync(join(serverDir, JAR_NAME)) && args.dir) {
|
|
37
|
+
log.error("A server already exists here");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const sp = spin("Fetching versions...");
|
|
42
|
+
const releases = await listReleases();
|
|
43
|
+
sp.stop(`${String(releases.length)} versions found`);
|
|
44
|
+
if (releases.length === 0)
|
|
45
|
+
throw new Error("No releases available");
|
|
46
|
+
let release;
|
|
47
|
+
if (args.version) {
|
|
48
|
+
const wanted = args.version.replace(/^v/, '');
|
|
49
|
+
release = releases.find(r => r.tag === args.version || r.version === wanted);
|
|
50
|
+
if (!release)
|
|
51
|
+
throw new Error(`Version ${args.version} not found`);
|
|
52
|
+
}
|
|
53
|
+
else if (isJson()) {
|
|
54
|
+
release = releases[0];
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const chosen = await promptSelect("Select a version", releases.map((r, i) => ({
|
|
58
|
+
value: r.tag,
|
|
59
|
+
label: i === 0 ? `${r.tag} (${"latest"})` : r.tag,
|
|
60
|
+
hint: r.prerelease ? "pre-release" : undefined,
|
|
61
|
+
})));
|
|
62
|
+
release = releases.find(r => r.tag === chosen) ?? releases[0];
|
|
63
|
+
}
|
|
64
|
+
await mkdir(serverDir, { recursive: true });
|
|
65
|
+
log.step("Downloading");
|
|
66
|
+
const { size } = await downloadCore(release, join(serverDir, JAR_NAME), pct => progress(pct, JAR_NAME));
|
|
67
|
+
log.success(`Downloaded (${(size / 1024 / 1024).toFixed(1)} MB)`);
|
|
68
|
+
const sp2 = spin("Downloading start scripts...");
|
|
69
|
+
for (const s of startScripts())
|
|
70
|
+
await downloadFile(s.url, join(serverDir, s.name));
|
|
71
|
+
sp2.stop("Start scripts downloaded");
|
|
72
|
+
await setDefaultServer(serverDir);
|
|
73
|
+
log.success("Server ready!");
|
|
74
|
+
if (await promptConfirm("Start the server now?", false)) {
|
|
75
|
+
const { runStart } = await import('./start.js');
|
|
76
|
+
await runStart({ dir: serverDir });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
outro("Server ready!");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
log.error(e.message);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { defineCommand } from 'citty';
|
|
3
|
+
import { intro, outro, log, spin, progress } from '../ui/output.js';
|
|
4
|
+
import { promptText } from '../ui/prompts.js';
|
|
5
|
+
import { resolveServerDir } from '../services/server.js';
|
|
6
|
+
import { installApprovedPlugin } from '../services/plugins.js';
|
|
7
|
+
export async function runInstall(slug) {
|
|
8
|
+
intro(`📦 ${"Install plugin"}`);
|
|
9
|
+
let name = slug?.trim();
|
|
10
|
+
if (!name) {
|
|
11
|
+
const answer = await promptText({ message: "Plugin slug? (owner.name)", placeholder: 'Owner.Plugin' });
|
|
12
|
+
name = answer?.trim();
|
|
13
|
+
}
|
|
14
|
+
if (!name) {
|
|
15
|
+
log.info("No plugin specified");
|
|
16
|
+
outro('');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const serverPath = (await resolveServerDir()) ?? '.';
|
|
20
|
+
const pluginsDir = join(serverPath, 'plugins');
|
|
21
|
+
const sp = spin(name);
|
|
22
|
+
try {
|
|
23
|
+
const entry = await installApprovedPlugin(name, pluginsDir, (pct, f) => progress(pct, f));
|
|
24
|
+
sp.stop(`${entry.name} v${entry.version}`);
|
|
25
|
+
log.success(`Installed ${entry.name}`);
|
|
26
|
+
log.info("Restart the server to load plugins");
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const msg = e?.message ?? '';
|
|
30
|
+
if (msg.startsWith('not-approved'))
|
|
31
|
+
sp.fail(`${name} is not approved or does not exist`);
|
|
32
|
+
else if (msg.startsWith('no-file'))
|
|
33
|
+
sp.fail(`No file for ${name}`);
|
|
34
|
+
else
|
|
35
|
+
sp.fail(`Failed to install ${name}`);
|
|
36
|
+
}
|
|
37
|
+
outro('');
|
|
38
|
+
}
|
|
39
|
+
export const installCmd = defineCommand({
|
|
40
|
+
meta: { name: 'install', description: 'Install an approved plugin by slug (owner.name)' },
|
|
41
|
+
args: { slug: { type: 'positional', description: 'Plugin slug, e.g. RedstoneCloud.CloudBridge', required: false } },
|
|
42
|
+
async run({ args }) {
|
|
43
|
+
await runInstall(args.slug);
|
|
44
|
+
},
|
|
45
|
+
});
|