@powernukkitx/cli 0.0.3 → 1.0.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.
Files changed (82) hide show
  1. package/README.md +203 -177
  2. package/dist/bundle.js +3 -18856
  3. package/dist/cli.js +46 -0
  4. package/dist/commands/backup.d.ts +2 -1
  5. package/dist/commands/backup.js +21 -71
  6. package/dist/commands/cfg.d.ts +2 -0
  7. package/dist/commands/cfg.js +61 -0
  8. package/dist/commands/config.js +52 -105
  9. package/dist/commands/console.d.ts +2 -0
  10. package/dist/commands/console.js +99 -0
  11. package/dist/commands/doctor.d.ts +2 -1
  12. package/dist/commands/doctor.js +87 -108
  13. package/dist/commands/info.d.ts +2 -1
  14. package/dist/commands/info.js +39 -33
  15. package/dist/commands/init.d.ts +2 -4
  16. package/dist/commands/init.js +58 -65
  17. package/dist/commands/lang.d.ts +2 -0
  18. package/dist/commands/lang.js +28 -0
  19. package/dist/commands/logs.d.ts +2 -0
  20. package/dist/commands/logs.js +61 -0
  21. package/dist/commands/menu.js +86 -0
  22. package/dist/commands/plugin/index.d.ts +2 -0
  23. package/dist/commands/plugin/index.js +13 -0
  24. package/dist/commands/plugin/info.d.ts +2 -0
  25. package/dist/commands/plugin/info.js +40 -0
  26. package/dist/commands/plugin/install.d.ts +2 -0
  27. package/dist/commands/plugin/install.js +57 -0
  28. package/dist/commands/plugin/installed.d.ts +2 -0
  29. package/dist/commands/plugin/installed.js +42 -0
  30. package/dist/commands/plugin/list.d.ts +2 -0
  31. package/dist/commands/plugin/list.js +49 -0
  32. package/dist/commands/plugin/remove.d.ts +2 -0
  33. package/dist/commands/plugin/remove.js +45 -0
  34. package/dist/commands/plugin/search.d.ts +2 -0
  35. package/dist/commands/plugin/search.js +12 -0
  36. package/dist/commands/plugin/update.d.ts +2 -0
  37. package/dist/commands/plugin/update.js +53 -0
  38. package/dist/commands/plugin.d.ts +1 -12
  39. package/dist/commands/plugin.js +1 -385
  40. package/dist/commands/start.d.ts +2 -7
  41. package/dist/commands/start.js +61 -94
  42. package/dist/commands/stop.d.ts +2 -0
  43. package/dist/commands/stop.js +27 -0
  44. package/dist/commands/update.d.ts +2 -5
  45. package/dist/commands/update.js +30 -50
  46. package/dist/commands/use.d.ts +2 -0
  47. package/dist/commands/use.js +40 -0
  48. package/dist/core/config.d.ts +13 -0
  49. package/dist/core/config.js +31 -0
  50. package/dist/core/network.d.ts +13 -0
  51. package/dist/core/network.js +139 -0
  52. package/dist/core/output.d.ts +33 -0
  53. package/dist/core/output.js +75 -0
  54. package/dist/core/process.d.ts +10 -0
  55. package/dist/core/process.js +53 -0
  56. package/dist/i18n/en.json +174 -0
  57. package/dist/i18n/fr.json +174 -0
  58. package/dist/i18n/index.js +58 -0
  59. package/dist/index.d.ts +0 -1
  60. package/dist/index.js +1 -260
  61. package/dist/infra/http.js +121 -0
  62. package/dist/infra/paths.js +20 -0
  63. package/dist/infra/store.js +27 -0
  64. package/dist/lang/en.json +116 -148
  65. package/dist/lang/fr.json +115 -147
  66. package/dist/main.d.ts +2 -0
  67. package/dist/main.js +28 -0
  68. package/dist/services/backup.js +40 -0
  69. package/dist/services/plugins.js +111 -0
  70. package/dist/services/process.js +43 -0
  71. package/dist/services/release.js +38 -0
  72. package/dist/services/server.js +89 -0
  73. package/dist/ui/output.js +71 -0
  74. package/dist/ui/prompts.js +30 -0
  75. package/dist/ui/theme.js +18 -0
  76. package/dist/utils/api.d.ts +3 -12
  77. package/dist/utils/api.js +7 -57
  78. package/dist/utils/github.d.ts +3 -10
  79. package/dist/utils/github.js +27 -154
  80. package/dist/utils/server.d.ts +2 -2
  81. package/dist/utils/server.js +17 -24
  82. package/package.json +15 -32
@@ -1,119 +1,98 @@
1
1
  import { existsSync, statSync, readdirSync } from 'node:fs';
2
+ import { statfs } from 'node:fs/promises';
2
3
  import { join } from 'node:path';
3
- import { spawn } from 'node:child_process';
4
+ import { spawnSync } from 'node:child_process';
4
5
  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';
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
+ import { t } from '../i18n/index.js';
10
12
  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); });
13
+ return new Promise(res => {
14
+ const s = createSocket('udp4');
15
+ s.once('error', () => { s.close(); res(false); });
16
+ s.bind(port, '0.0.0.0', () => { s.close(); res(true); });
15
17
  });
16
18
  }
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'));
19
+ function pnxVersion(jarPath) {
20
+ try {
21
+ const r = spawnSync('java', ['-jar', jarPath, '--version'], { timeout: 3000, encoding: 'utf8' });
22
+ return ((r.stdout || '') + (r.stderr || '')).match(/(\d+\.\d+\.\d+[\w.-]*)/)?.[1] ?? null;
78
23
  }
79
- else {
80
- warn(t('doctor.configMissing'));
24
+ catch {
25
+ return null;
81
26
  }
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'));
27
+ }
28
+ async function diskFreeMb(dir) {
29
+ try {
30
+ const s = await statfs(dir);
31
+ return Math.round((s.bfree * s.bsize) / 1024 / 1024);
112
32
  }
113
- else {
114
- warn(t('doctor.portInUse', '19132'));
33
+ catch {
34
+ return null;
115
35
  }
116
- divider();
117
- success(t('doctor.done'));
118
- await pauseForExit();
119
36
  }
37
+ export const doctorCmd = defineCommand({
38
+ meta: { description: 'Diagnose server health' },
39
+ args: {},
40
+ async run() {
41
+ const sp = spin(t('doctor.checking'));
42
+ const java = await checkJava();
43
+ const serverPath = await resolveServerDir();
44
+ const jarPath = serverPath ? join(serverPath, JAR_NAME) : null;
45
+ const jarExists = jarPath ? existsSync(jarPath) : false;
46
+ const version = jarPath && jarExists ? pnxVersion(jarPath) : null;
47
+ const pluginsDir = serverPath ? join(serverPath, 'plugins') : null;
48
+ const worldsDir = serverPath ? join(serverPath, 'worlds') : null;
49
+ const pluginCount = pluginsDir && existsSync(pluginsDir)
50
+ ? readdirSync(pluginsDir).filter(f => f.endsWith('.jar')).length : 0;
51
+ const worldCount = worldsDir && existsSync(worldsDir)
52
+ ? readdirSync(worldsDir).filter(f => statSync(join(worldsDir, f)).isDirectory()).length : 0;
53
+ const freeMb = Math.round(freemem() / 1024 / 1024);
54
+ const totalMb = Math.round(totalmem() / 1024 / 1024);
55
+ const disk = serverPath ? await diskFreeMb(serverPath) : null;
56
+ const portFree = await checkPort(19132);
57
+ sp.stop(t('doctor.done'));
58
+ if (isJson())
59
+ json({
60
+ java: { ok: java.ok, version: java.raw, vendor: java.vendor },
61
+ server: { found: !!serverPath, path: serverPath, pnxVersion: version },
62
+ ram: { freeMb, totalMb },
63
+ disk: disk !== null ? { freeMb: disk } : null,
64
+ port: { free: portFree, port: 19132 },
65
+ plugins: { count: pluginCount },
66
+ worlds: { count: worldCount },
67
+ });
68
+ intro(`🩺 ${t('doctor.title')}`);
69
+ section(t('doctor.java'));
70
+ if (java.ok)
71
+ log.success(`${java.raw} (${java.vendor})`);
72
+ else {
73
+ log.error(t('doctor.javaMissing'));
74
+ log.info('https://adoptium.net/');
75
+ }
76
+ section(t('doctor.server'));
77
+ if (serverPath) {
78
+ log.success(serverPath);
79
+ if (jarExists)
80
+ log.success(`${JAR_NAME}${version ? ` v${version}` : ''}`);
81
+ else
82
+ log.error(t('doctor.jarMissing'));
83
+ }
84
+ else
85
+ log.error(t('doctor.noServer'));
86
+ section(t('doctor.plugins'));
87
+ log.info(`Plugins: ${pluginCount} | Worlds: ${worldCount}`);
88
+ section(t('doctor.system'));
89
+ log.info(`RAM: ${freeMb} Mo / ${totalMb} Mo`);
90
+ if (disk !== null)
91
+ log.info(`Disk: ${disk} Mo`);
92
+ if (portFree)
93
+ log.success(t('doctor.portFree', '19132'));
94
+ else
95
+ log.warn(t('doctor.portBusy', '19132'));
96
+ outro(t('doctor.done'));
97
+ },
98
+ });
@@ -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,42 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readdir } from 'node:fs/promises';
3
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
- }
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
+ import { t } from '../i18n/index.js';
10
+ export const infoCmd = defineCommand({
11
+ meta: { description: 'Show server information' },
12
+ args: {},
13
+ async run() {
14
+ const serverPath = await resolveServerDir();
15
+ const installed = serverPath !== null;
16
+ let plugins = [], worlds = [];
17
+ if (serverPath && existsSync(join(serverPath, 'plugins'))) {
18
+ plugins = (await readdir(join(serverPath, 'plugins'))).filter(f => f.endsWith('.jar'));
19
+ }
20
+ if (serverPath && existsSync(join(serverPath, 'worlds'))) {
21
+ worlds = await readdir(join(serverPath, 'worlds'));
22
+ }
23
+ const freeMb = Math.round(freemem() / 1024 / 1024);
24
+ const totalMb = Math.round(totalmem() / 1024 / 1024);
25
+ if (isJson())
26
+ json({
27
+ status: installed ? 'installed' : 'not_installed',
28
+ serverDir: serverPath, plugins: plugins.length, worlds: worlds.length,
29
+ ram: { freeMb, totalMb },
30
+ });
31
+ intro(`📊 ${t('info.title')}`);
32
+ section(t('info.server'));
33
+ log.info(`${t('info.status').padEnd(12)} ${installed ? c.green('✔ ' + t('info.installed')) : c.red('✘ ' + t('info.notInstalled'))}`);
34
+ if (serverPath)
35
+ log.info(`${t('info.path').padEnd(12)} ${c.accent(serverPath)}`);
36
+ log.info(`${t('info.plugins').padEnd(12)} ${plugins.length}`);
37
+ log.info(`${t('info.worlds').padEnd(12)} ${worlds.length}`);
38
+ section(t('info.system'));
39
+ log.info(`${t('info.ram').padEnd(12)} ${freeMb} Mo / ${totalMb} Mo`);
40
+ outro(installed ? t('info.ready') : t('info.notReady'));
41
+ },
42
+ });
@@ -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;
@@ -2,72 +2,65 @@ import { existsSync } from 'node:fs';
2
2
  import { mkdir } from 'node:fs/promises';
3
3
  import { join, resolve } from 'node:path';
4
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.');
5
+ import { defineCommand } from 'citty';
6
+ import { intro, outro, log, spin, progress } from '../ui/output.js';
7
+ import { promptText, promptConfirm } from '../ui/prompts.js';
8
+ import { getLatestRelease, downloadCore } from '../services/release.js';
9
+ import { startScripts } from '../services/server.js';
10
+ import { downloadFile } from '../infra/http.js';
11
+ import { JAR_NAME } from '../infra/paths.js';
12
+ import { t } from '../i18n/index.js';
13
+ export const initCmd = defineCommand({
14
+ meta: { description: 'Create a new PowerNukkitX server' },
15
+ args: {
16
+ dir: { type: 'string', description: 'Target directory' },
17
+ dev: { type: 'boolean', description: 'Install dev version', default: false },
18
+ },
19
+ async run({ args }) {
20
+ intro(`⚡ ${t('init.title')}`);
21
+ let target = args.dir;
22
+ if (!target) {
23
+ target = await promptText({
24
+ message: t('init.promptDir'),
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 t('init.alreadyExists');
31
+ return undefined;
32
+ },
33
+ });
34
+ }
35
+ const serverDir = resolve(cwd(), target);
36
+ if (existsSync(join(serverDir, JAR_NAME)) && args.dir) {
37
+ log.error(t('init.alreadyExists'));
40
38
  process.exit(1);
41
39
  }
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);
40
+ try {
41
+ const sp = spin(t('init.fetchingInfo'));
42
+ const release = await getLatestRelease();
43
+ sp.stop(`${t('init.latestVersion')}: ${release.tag}`);
44
+ await mkdir(serverDir, { recursive: true });
45
+ log.step(t('init.downloading'));
46
+ const { size } = await downloadCore(release, join(serverDir, JAR_NAME), pct => progress(pct, JAR_NAME));
47
+ log.success(t('init.downloaded', (size / 1024 / 1024).toFixed(1)));
48
+ const sp2 = spin(t('init.downloadingScripts'));
49
+ for (const s of startScripts())
50
+ await downloadFile(s.url, join(serverDir, s.name));
51
+ sp2.stop(t('init.scriptsDownloaded'));
52
+ log.success(t('init.success'));
53
+ if (await promptConfirm(t('init.launchPrompt'), false)) {
54
+ const { runStart } = await import('./start.js');
55
+ await runStart({ dir: serverDir });
56
+ }
57
+ else {
58
+ outro(t('init.success'));
59
+ }
51
60
  }
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({});
61
+ catch (e) {
62
+ log.error(e.message);
63
+ process.exit(1);
67
64
  }
68
- }
69
- catch (err) {
70
- error(err.message);
71
- process.exit(1);
72
- }
73
- }
65
+ },
66
+ });
@@ -0,0 +1,2 @@
1
+ import type { CommandDef } from 'citty';
2
+ export declare const langCmd: CommandDef;
@@ -0,0 +1,28 @@
1
+ import { defineCommand } from 'citty';
2
+ import { intro, outro, log } from '../ui/output.js';
3
+ import { promptSelect } from '../ui/prompts.js';
4
+ import { setLang, t } from '../i18n/index.js';
5
+ import { readJson, writeJson } from '../infra/store.js';
6
+ async function persistLang(code) {
7
+ const cfg = await readJson('config.json', {});
8
+ cfg.lang = code;
9
+ await writeJson('config.json', cfg);
10
+ }
11
+ export const langCmd = defineCommand({
12
+ meta: { description: 'Change language / Changer la langue' },
13
+ args: { set: { type: 'string', description: 'Language code (en, fr)' } },
14
+ async run({ args }) {
15
+ intro(`🌐 ${t('lang.title')}`);
16
+ let lang = args.set;
17
+ if (lang !== 'en' && lang !== 'fr') {
18
+ lang = await promptSelect(t('lang.prompt'), [
19
+ { value: 'fr', label: '🇫🇷 Français' },
20
+ { value: 'en', label: '🇬🇧 English' },
21
+ ]);
22
+ }
23
+ setLang(lang);
24
+ await persistLang(lang);
25
+ log.success(t('lang.set', lang));
26
+ outro(t('lang.set', lang));
27
+ },
28
+ });
@@ -0,0 +1,2 @@
1
+ import type { CommandDef } from 'citty';
2
+ export declare const logsCmd: CommandDef;
@@ -0,0 +1,61 @@
1
+ import { existsSync, readdirSync, statSync, readFileSync, watch } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { defineCommand } from 'citty';
4
+ import { intro, log } from '../ui/output.js';
5
+ import { c } from '../ui/theme.js';
6
+ import { resolveServerDir } from '../services/server.js';
7
+ import { t } from '../i18n/index.js';
8
+ export const logsCmd = defineCommand({
9
+ meta: { description: 'View server logs' },
10
+ args: {
11
+ lines: { type: 'string', description: 'Number of lines to show', default: '50' },
12
+ follow: { type: 'boolean', description: 'Follow log output', default: true },
13
+ },
14
+ async run({ args }) {
15
+ intro(`📋 ${t('logs.title')}`);
16
+ const serverPath = await resolveServerDir();
17
+ if (!serverPath) {
18
+ log.error(t('logs.noServer'));
19
+ process.exit(1);
20
+ }
21
+ const logsDir = join(serverPath, 'logs');
22
+ if (!existsSync(logsDir)) {
23
+ log.error(t('logs.noLogs'));
24
+ process.exit(1);
25
+ }
26
+ const files = readdirSync(logsDir)
27
+ .filter(f => f.endsWith('.log') || f === 'latest.log')
28
+ .map(f => ({ name: f, mtime: statSync(join(logsDir, f)).mtime.getTime() }))
29
+ .sort((a, b) => b.mtime - a.mtime);
30
+ if (files.length === 0) {
31
+ log.error(t('logs.noLogs'));
32
+ process.exit(1);
33
+ }
34
+ const logPath = join(logsDir, files[0].name);
35
+ log.step(t('logs.watching', files[0].name));
36
+ const n = parseInt(args.lines) || 50;
37
+ const content = readFileSync(logPath, 'utf-8');
38
+ const colorize = (line) => line.replace(/\[ERROR\]/g, c.red('[ERROR]'))
39
+ .replace(/\[WARN\]/g, c.yellow('[WARN]'))
40
+ .replace(/\[INFO\]/g, c.blue('[INFO]'));
41
+ for (const line of content.split('\n').filter(Boolean).slice(-n)) {
42
+ process.stdout.write(colorize(line) + '\n');
43
+ }
44
+ if (args.follow) {
45
+ log.dim(t('logs.liveMode'));
46
+ process.on('SIGINT', () => { process.stdout.write('\n'); process.exit(0); });
47
+ let buf = content;
48
+ watch(logPath, () => {
49
+ try {
50
+ const updated = readFileSync(logPath, 'utf-8');
51
+ if (updated.length > buf.length) {
52
+ process.stdout.write(updated.slice(buf.length));
53
+ buf = updated;
54
+ }
55
+ }
56
+ catch { /* rotation en cours */ }
57
+ });
58
+ await new Promise(() => { });
59
+ }
60
+ },
61
+ });
@@ -0,0 +1,86 @@
1
+ import { intro, outro, log } from '../ui/output.js';
2
+ import { promptSelect } from '../ui/prompts.js';
3
+ import { detectServers } from '../services/server.js';
4
+ import { loadPid, isAlive } from '../services/process.js';
5
+ import { t } from '../i18n/index.js';
6
+ /** Menu interactif racine, contextuel à l'état du serveur. */
7
+ export async function runMenu() {
8
+ const servers = detectServers();
9
+ const hasServer = servers.length > 0;
10
+ const pid = await loadPid();
11
+ const running = pid !== null && isAlive(pid.pid);
12
+ intro(`⚡ ${t('menu.title')}`);
13
+ if (running)
14
+ log.info(t('menu.statusRunning', String(pid.pid)));
15
+ else if (hasServer)
16
+ log.info(t('menu.statusStopped'));
17
+ else
18
+ log.warn(t('menu.statusNoServer'));
19
+ const options = [];
20
+ if (!hasServer)
21
+ options.push({ value: 'create', label: t('menu.create') });
22
+ if (hasServer && !running)
23
+ options.push({ value: 'start', label: t('menu.start') });
24
+ if (running)
25
+ options.push({ value: 'stop', label: t('menu.stop') });
26
+ if (hasServer) {
27
+ options.push({ value: 'plugins', label: t('menu.plugins') });
28
+ options.push({ value: 'update', label: t('menu.update') });
29
+ options.push({ value: 'backup', label: t('menu.backup') });
30
+ }
31
+ options.push({ value: 'doctor', label: t('menu.doctor') });
32
+ options.push({ value: 'info', label: t('menu.info') });
33
+ options.push({ value: 'lang', label: t('menu.lang') });
34
+ options.push({ value: 'quit', label: t('menu.quit') });
35
+ const choice = await promptSelect(t('menu.prompt'), options);
36
+ switch (choice) {
37
+ case 'create': {
38
+ const { initCmd } = await import('./init.js');
39
+ await initCmd.run({ args: {} });
40
+ break;
41
+ }
42
+ case 'start': {
43
+ const { runStart } = await import('./start.js');
44
+ await runStart({});
45
+ break;
46
+ }
47
+ case 'stop': {
48
+ const { runStop } = await import('./stop.js');
49
+ await runStop();
50
+ break;
51
+ }
52
+ case 'plugins': {
53
+ const { installCmd } = await import('./plugin/install.js');
54
+ await installCmd.run({ args: {} });
55
+ break;
56
+ }
57
+ case 'update': {
58
+ const { runUpdate } = await import('./update.js');
59
+ await runUpdate();
60
+ break;
61
+ }
62
+ case 'backup': {
63
+ const { runBackup } = await import('./backup.js');
64
+ await runBackup();
65
+ break;
66
+ }
67
+ case 'doctor': {
68
+ const { doctorCmd } = await import('./doctor.js');
69
+ await doctorCmd.run({ args: {} });
70
+ break;
71
+ }
72
+ case 'info': {
73
+ const { infoCmd } = await import('./info.js');
74
+ await infoCmd.run({ args: {} });
75
+ break;
76
+ }
77
+ case 'lang': {
78
+ const { langCmd } = await import('./lang.js');
79
+ await langCmd.run({ args: {} });
80
+ break;
81
+ }
82
+ case 'quit':
83
+ outro(t('menu.bye'));
84
+ break;
85
+ }
86
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandDef } from 'citty';
2
+ export declare const pluginCmd: CommandDef;
@@ -0,0 +1,13 @@
1
+ import { defineCommand } from 'citty';
2
+ export const pluginCmd = defineCommand({
3
+ meta: { description: 'Plugin management' },
4
+ subCommands: {
5
+ install: () => import('./install.js').then(m => m.installCmd),
6
+ list: () => import('./list.js').then(m => m.listCmd),
7
+ search: () => import('./search.js').then(m => m.searchCmd),
8
+ info: () => import('./info.js').then(m => m.infoCmd),
9
+ update: () => import('./update.js').then(m => m.updateCmd),
10
+ remove: () => import('./remove.js').then(m => m.removeCmd),
11
+ installed: () => import('./installed.js').then(m => m.installedCmd),
12
+ },
13
+ });
@@ -0,0 +1,2 @@
1
+ import type { CommandDef } from 'citty';
2
+ export declare const infoCmd: CommandDef;