@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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -101
  3. package/dist/cli.js +34 -0
  4. package/dist/commands/backup.d.ts +2 -1
  5. package/dist/commands/backup.js +23 -74
  6. package/dist/commands/config.d.ts +1 -1
  7. package/dist/commands/config.js +60 -114
  8. package/dist/commands/console.d.ts +2 -0
  9. package/dist/commands/console.js +98 -0
  10. package/dist/commands/doctor.d.ts +2 -1
  11. package/dist/commands/doctor.js +97 -119
  12. package/dist/commands/info.d.ts +2 -1
  13. package/dist/commands/info.js +41 -36
  14. package/dist/commands/init.d.ts +2 -4
  15. package/dist/commands/init.js +87 -73
  16. package/dist/commands/install.js +45 -0
  17. package/dist/commands/logs.d.ts +2 -0
  18. package/dist/commands/logs.js +60 -0
  19. package/dist/commands/menu.js +94 -0
  20. package/dist/commands/plugin/index.d.ts +2 -0
  21. package/dist/commands/plugin/index.js +13 -0
  22. package/dist/commands/plugin/info.d.ts +2 -0
  23. package/dist/commands/plugin/info.js +39 -0
  24. package/dist/commands/plugin/install.d.ts +2 -0
  25. package/dist/commands/plugin/install.js +56 -0
  26. package/dist/commands/plugin/installed.d.ts +2 -0
  27. package/dist/commands/plugin/installed.js +41 -0
  28. package/dist/commands/plugin/list.d.ts +2 -0
  29. package/dist/commands/plugin/list.js +47 -0
  30. package/dist/commands/plugin/remove.d.ts +2 -0
  31. package/dist/commands/plugin/remove.js +44 -0
  32. package/dist/commands/plugin/search.d.ts +2 -0
  33. package/dist/commands/plugin/search.js +11 -0
  34. package/dist/commands/plugin/update.d.ts +2 -0
  35. package/dist/commands/plugin/update.js +52 -0
  36. package/dist/commands/start.d.ts +2 -7
  37. package/dist/commands/start.js +74 -108
  38. package/dist/commands/stop.d.ts +2 -0
  39. package/dist/commands/stop.js +26 -0
  40. package/dist/commands/update.d.ts +2 -5
  41. package/dist/commands/update.js +34 -55
  42. package/dist/commands/use.d.ts +2 -0
  43. package/dist/commands/use.js +39 -0
  44. package/dist/infra/http.js +119 -0
  45. package/dist/infra/paths.js +15 -0
  46. package/dist/infra/store.js +24 -0
  47. package/dist/services/backup.js +34 -0
  48. package/dist/services/plugins.js +139 -0
  49. package/dist/services/process.js +41 -0
  50. package/dist/services/release.js +51 -0
  51. package/dist/services/server.js +81 -0
  52. package/dist/ui/output.js +67 -0
  53. package/dist/ui/prompts.js +26 -0
  54. package/dist/ui/theme.js +16 -0
  55. package/package.json +44 -63
  56. package/dist/bundle.js +0 -18871
  57. package/dist/commands/plugin.d.ts +0 -12
  58. package/dist/commands/plugin.js +0 -385
  59. package/dist/index.d.ts +0 -2
  60. package/dist/index.js +0 -260
  61. package/dist/lang/en.json +0 -178
  62. package/dist/lang/fr.json +0 -178
  63. package/dist/lang/index.d.ts +0 -7
  64. package/dist/lang/index.js +0 -83
  65. package/dist/plugins.json +0 -66
  66. package/dist/types.d.ts +0 -61
  67. package/dist/types.js +0 -1
  68. package/dist/utils/api.d.ts +0 -18
  69. package/dist/utils/api.js +0 -74
  70. package/dist/utils/github.d.ts +0 -27
  71. package/dist/utils/github.js +0 -187
  72. package/dist/utils/logger.d.ts +0 -15
  73. package/dist/utils/logger.js +0 -72
  74. package/dist/utils/pause.d.ts +0 -1
  75. package/dist/utils/pause.js +0 -6
  76. package/dist/utils/plugins.d.ts +0 -4
  77. package/dist/utils/plugins.js +0 -34
  78. package/dist/utils/server.d.ts +0 -3
  79. package/dist/utils/server.js +0 -39
  80. package/dist/utils/updater.d.ts +0 -1
  81. 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
- export declare function doctorCmd(): Promise<void>;
1
+ import type { CommandDef } from 'citty';
2
+ export declare const doctorCmd: CommandDef;
@@ -1,119 +1,97 @@
1
- import { existsSync, statSync, readdirSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { spawn } from 'node:child_process';
4
- import { createSocket } from 'node:dgram';
5
- import { header, success, error, info, warn, highlight, dim, step, divider } from '../utils/logger.js';
6
- import { t } from '../lang/index.js';
7
- import { resolveServerDir } from '../utils/server.js';
8
- import { getInstalledPlugins } from '../utils/plugins.js';
9
- import { pauseForExit } from '../utils/pause.js';
10
- function checkPort(port) {
11
- return new Promise((resolve) => {
12
- const socket = createSocket('udp4');
13
- socket.once('error', () => { socket.close(); resolve(false); });
14
- socket.bind(port, '0.0.0.0', () => { socket.close(); resolve(true); });
15
- });
16
- }
17
- function checkJavaVersion() {
18
- return new Promise((resolve) => {
19
- const java = process.env.JAVA_HOME
20
- ? join(process.env.JAVA_HOME, 'bin', 'java')
21
- : 'java';
22
- const child = spawn(java, ['-version']);
23
- let output = '';
24
- child.on('error', () => resolve({ ok: false, version: '?', path: java }));
25
- child.stderr.on('data', (data) => { output += data.toString(); });
26
- child.on('close', () => {
27
- const match = output.match(/(?:openjdk|java) version "?(?:1\.)?(\d+)/);
28
- if (match) {
29
- const ver = parseInt(match[1], 10);
30
- resolve({ ok: ver >= 21, version: output.split('\n')[0]?.trim() || String(ver), path: java });
31
- }
32
- else {
33
- resolve({ ok: false, version: '?', path: java });
34
- }
35
- });
36
- });
37
- }
38
- export async function doctorCmd() {
39
- header(`🩺 ${t('doctor.title')}`);
40
- // Java check
41
- step(t('doctor.checkingJava'));
42
- const java = await checkJavaVersion();
43
- if (java.ok) {
44
- success(t('doctor.javaOk', java.version));
45
- }
46
- else {
47
- warn(`Java ${highlight('21+')} required — ${dim(java.path)}`);
48
- }
49
- // Server detection
50
- step(t('doctor.checkingServer'));
51
- const serverPath = await resolveServerDir();
52
- if (!serverPath) {
53
- error(t('doctor.serverNotFound'));
54
- await pauseForExit();
55
- return;
56
- }
57
- success(`${t('doctor.serverFound')} ${dim(serverPath)}`);
58
- // JAR check
59
- step(t('doctor.checkingJar'));
60
- const jarPath = join(serverPath, 'powernukkitx.jar');
61
- if (existsSync(jarPath)) {
62
- const size = statSync(jarPath).size;
63
- const sizeMb = (size / 1024 / 1024).toFixed(1);
64
- success(t('doctor.jarOk', sizeMb));
65
- }
66
- else {
67
- error(t('doctor.jarMissing'));
68
- }
69
- // pnx.yml check
70
- step(t('doctor.checkingConfig'));
71
- const ymlPath = join(serverPath, 'pnx.yml');
72
- const propsPath = join(serverPath, 'server.properties');
73
- if (existsSync(ymlPath)) {
74
- success(t('doctor.configFound'));
75
- }
76
- else if (existsSync(propsPath)) {
77
- info(t('doctor.configLegacy'));
78
- }
79
- else {
80
- warn(t('doctor.configMissing'));
81
- }
82
- // plugins dir
83
- step(t('doctor.checkingPlugins'));
84
- const pluginsDir = join(serverPath, 'plugins');
85
- if (existsSync(pluginsDir)) {
86
- const files = readdirSync(pluginsDir).filter(f => f.endsWith('.jar'));
87
- success(t('doctor.pluginsOk', String(files.length)));
88
- // Check for updates
89
- const tracked = await getInstalledPlugins();
90
- if (tracked.length > 0) {
91
- info(t('doctor.pluginCheckHint', 'pnx plugin update'));
92
- }
93
- }
94
- else {
95
- info(t('doctor.pluginsMissing'));
96
- }
97
- // worlds dir
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
+ });
@@ -1 +1,2 @@
1
- export declare function infoCmd(): Promise<void>;
1
+ import type { CommandDef } from 'citty';
2
+ export declare const infoCmd: CommandDef;
@@ -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 chalk from 'chalk';
5
- import { header, info, warn, table, highlight } from '../utils/logger.js';
6
- import { t } from '../lang/index.js';
7
- import { resolveServerDir } from '../utils/server.js';
8
- import { pauseForExit } from '../utils/pause.js';
9
- export async function infoCmd() {
10
- header(`📊 ${t('info.title')}`);
11
- const serverPath = await resolveServerDir();
12
- const jarExists = serverPath !== null;
13
- let plugins = [];
14
- let worlds = [];
15
- if (serverPath && existsSync(join(serverPath, 'plugins'))) {
16
- const files = await readdir(join(serverPath, 'plugins'));
17
- plugins = files.filter((f) => f.endsWith('.jar'));
18
- }
19
- if (serverPath && existsSync(join(serverPath, 'worlds'))) {
20
- worlds = await readdir(join(serverPath, 'worlds'));
21
- }
22
- table([
23
- [t('info.status'), jarExists ? chalk.green(`✔ ${t('info.installed')}`) : chalk.red(`✘ ${t('info.notInstalled')}`)],
24
- [t('info.serverDir'), serverPath ? highlight(serverPath) : chalk.dim('—')],
25
- [t('info.plugins'), `${plugins.length} ${t('info.plugins')}`],
26
- [t('info.worlds'), `${worlds.length} ${t('info.worlds')}`],
27
- ]);
28
- console.log();
29
- if (jarExists) {
30
- info(t('info.ready'));
31
- }
32
- else {
33
- warn(t('info.notInitialized'));
34
- }
35
- await pauseForExit();
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
+ });
@@ -1,4 +1,2 @@
1
- export declare function initCmd(options: {
2
- dir?: string;
3
- dev?: boolean;
4
- }): Promise<void>;
1
+ import type { CommandDef } from 'citty';
2
+ export declare const initCmd: CommandDef;
@@ -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 { input, confirm } from '@inquirer/prompts';
6
- import chalk from 'chalk';
7
- import { header, success, dim, step, error } from '../utils/logger.js';
8
- import { t } from '../lang/index.js';
9
- import { getLatestRelease, findAsset, downloadFile, getStartScripts } from '../utils/github.js';
10
- import { startCmd } from './start.js';
11
- export async function initCmd(options) {
12
- header(`⚡ ${t('init.title')}`);
13
- const targetDir = options.dir
14
- ? resolve(cwd(), options.dir)
15
- : resolve(cwd(), await input({
16
- message: t('init.promptDir'),
17
- default: 'pnx-server',
18
- validate: (v) => {
19
- if (!v.trim())
20
- return 'Path is required';
21
- const p = resolve(cwd(), v);
22
- if (existsSync(join(p, 'powernukkitx.jar'))) {
23
- return t('init.alreadyExists');
24
- }
25
- return true;
26
- },
27
- }));
28
- const serverDir = resolve(cwd(), targetDir);
29
- if (existsSync(join(serverDir, 'powernukkitx.jar'))) {
30
- error(t('init.alreadyExists'));
31
- process.exit(1);
32
- }
33
- try {
34
- step(t('init.fetchingInfo'));
35
- const release = await getLatestRelease();
36
- success(`${t('init.latestVersion')}: ${chalk.bold(release.tag_name)}`);
37
- const asset = await findAsset(release, 'powernukkitx.jar');
38
- if (!asset) {
39
- error('powernukkitx.jar not found in latest release.');
40
- process.exit(1);
41
- }
42
- await mkdir(serverDir, { recursive: true });
43
- const jarDest = join(serverDir, 'powernukkitx.jar');
44
- await downloadFile(asset.browser_download_url, jarDest, 'powernukkitx.jar');
45
- success(t('init.downloaded', (asset.size / 1024 / 1024).toFixed(1)));
46
- step(t('init.downloadingScripts'));
47
- const scripts = getStartScripts();
48
- for (const script of scripts) {
49
- const dest = join(serverDir, script.name);
50
- await downloadFile(script.url, dest, script.name);
51
- }
52
- success(t('init.scriptsDownloaded'));
53
- console.log(`\n ${chalk.hex('#FFB347').bold(t('init.nextSteps'))}`);
54
- console.log(` ${dim(' 1.')} ${chalk.hex('#FFB347')(`cd ${serverDir}`)}`);
55
- console.log(` ${dim(' 2.')} ${chalk.hex('#FFB347')('pnx start')} ${dim(`# ${t('init.nextStepStart')}`)}`);
56
- console.log(` ${dim(' 3.')} ${chalk.hex('#FFB347')('pnx plugin list')} ${dim(`# ${t('init.nextStepPlugins')}`)}`);
57
- console.log(` ${dim(' 4.')} ${chalk.hex('#FFB347')('pnx plugin install <name>')} ${dim(`# ${t('init.nextStepInstall')}`)}`);
58
- console.log();
59
- success(t('init.success'));
60
- const launchNow = await confirm({
61
- message: t('init.launchPrompt'),
62
- default: false,
63
- });
64
- if (launchNow) {
65
- console.log();
66
- await startCmd({});
67
- }
68
- }
69
- catch (err) {
70
- error(err.message);
71
- process.exit(1);
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
+ });
@@ -0,0 +1,2 @@
1
+ import type { CommandDef } from 'citty';
2
+ export declare const logsCmd: CommandDef;