@powernukkitx/cli 1.0.0 → 1.0.2

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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -203
  3. package/dist/cli.js +34 -46
  4. package/dist/commands/backup.d.ts +2 -2
  5. package/dist/commands/backup.js +23 -24
  6. package/dist/commands/config.d.ts +1 -1
  7. package/dist/commands/config.js +60 -61
  8. package/dist/commands/console.d.ts +2 -2
  9. package/dist/commands/console.js +98 -99
  10. package/dist/commands/doctor.d.ts +2 -2
  11. package/dist/commands/doctor.js +97 -98
  12. package/dist/commands/info.d.ts +2 -2
  13. package/dist/commands/info.js +41 -42
  14. package/dist/commands/init.d.ts +2 -2
  15. package/dist/commands/init.js +87 -66
  16. package/dist/commands/install.js +45 -0
  17. package/dist/commands/logs.d.ts +2 -2
  18. package/dist/commands/logs.js +60 -61
  19. package/dist/commands/menu.js +94 -86
  20. package/dist/commands/plugin/index.d.ts +2 -2
  21. package/dist/commands/plugin/index.js +13 -13
  22. package/dist/commands/plugin/info.d.ts +2 -2
  23. package/dist/commands/plugin/info.js +39 -40
  24. package/dist/commands/plugin/install.d.ts +2 -2
  25. package/dist/commands/plugin/install.js +56 -57
  26. package/dist/commands/plugin/installed.d.ts +2 -2
  27. package/dist/commands/plugin/installed.js +41 -42
  28. package/dist/commands/plugin/list.d.ts +2 -2
  29. package/dist/commands/plugin/list.js +47 -49
  30. package/dist/commands/plugin/remove.d.ts +2 -2
  31. package/dist/commands/plugin/remove.js +44 -45
  32. package/dist/commands/plugin/search.d.ts +2 -2
  33. package/dist/commands/plugin/search.js +11 -12
  34. package/dist/commands/plugin/update.d.ts +2 -2
  35. package/dist/commands/plugin/update.js +52 -53
  36. package/dist/commands/start.d.ts +2 -2
  37. package/dist/commands/start.js +74 -75
  38. package/dist/commands/stop.d.ts +2 -2
  39. package/dist/commands/stop.js +26 -27
  40. package/dist/commands/update.d.ts +2 -2
  41. package/dist/commands/update.js +34 -35
  42. package/dist/commands/use.d.ts +2 -2
  43. package/dist/commands/use.js +39 -40
  44. package/dist/infra/http.js +119 -121
  45. package/dist/infra/paths.js +15 -20
  46. package/dist/infra/store.js +24 -27
  47. package/dist/services/backup.js +34 -40
  48. package/dist/services/plugins.js +139 -111
  49. package/dist/services/process.js +41 -43
  50. package/dist/services/release.js +51 -38
  51. package/dist/services/server.js +81 -89
  52. package/dist/ui/output.js +67 -71
  53. package/dist/ui/prompts.js +26 -30
  54. package/dist/ui/theme.js +16 -18
  55. package/package.json +44 -46
  56. package/dist/bundle.js +0 -18
  57. package/dist/commands/cfg.d.ts +0 -2
  58. package/dist/commands/cfg.js +0 -61
  59. package/dist/commands/lang.d.ts +0 -2
  60. package/dist/commands/lang.js +0 -28
  61. package/dist/commands/plugin.d.ts +0 -1
  62. package/dist/commands/plugin.js +0 -1
  63. package/dist/core/config.d.ts +0 -13
  64. package/dist/core/config.js +0 -31
  65. package/dist/core/network.d.ts +0 -13
  66. package/dist/core/network.js +0 -139
  67. package/dist/core/output.d.ts +0 -33
  68. package/dist/core/output.js +0 -75
  69. package/dist/core/process.d.ts +0 -10
  70. package/dist/core/process.js +0 -53
  71. package/dist/i18n/en.json +0 -174
  72. package/dist/i18n/fr.json +0 -174
  73. package/dist/i18n/index.js +0 -58
  74. package/dist/index.d.ts +0 -1
  75. package/dist/index.js +0 -1
  76. package/dist/lang/en.json +0 -146
  77. package/dist/lang/fr.json +0 -146
  78. package/dist/lang/index.d.ts +0 -7
  79. package/dist/lang/index.js +0 -83
  80. package/dist/main.d.ts +0 -2
  81. package/dist/main.js +0 -28
  82. package/dist/plugins.json +0 -66
  83. package/dist/types.d.ts +0 -61
  84. package/dist/types.js +0 -1
  85. package/dist/utils/api.d.ts +0 -9
  86. package/dist/utils/api.js +0 -24
  87. package/dist/utils/github.d.ts +0 -20
  88. package/dist/utils/github.js +0 -60
  89. package/dist/utils/logger.d.ts +0 -15
  90. package/dist/utils/logger.js +0 -72
  91. package/dist/utils/pause.d.ts +0 -1
  92. package/dist/utils/pause.js +0 -6
  93. package/dist/utils/plugins.d.ts +0 -4
  94. package/dist/utils/plugins.js +0 -34
  95. package/dist/utils/server.d.ts +0 -3
  96. package/dist/utils/server.js +0 -32
  97. package/dist/utils/updater.d.ts +0 -1
  98. package/dist/utils/updater.js +0 -86
package/dist/ui/output.js CHANGED
@@ -1,71 +1,67 @@
1
- import * as clack from '@clack/prompts';
2
- import { c, chalk } from './theme.js';
3
- const _isJson = process.argv.includes('--json');
4
- /** True si le CLI tourne en mode machine (--json). */
5
- export function isJson() { return _isJson; }
6
- /** Émet un objet JSON sur stdout et quitte (mode --json). */
7
- export function json(data) {
8
- process.stdout.write(JSON.stringify(data, null, 2) + '\n');
9
- process.exit(0);
10
- }
11
- export function intro(title) {
12
- if (_isJson)
13
- return;
14
- clack.intro(c.primary.bold(` ${title} `));
15
- }
16
- export function outro(msg) {
17
- if (_isJson)
18
- return;
19
- clack.outro(c.accent(msg));
20
- }
21
- export function section(title) {
22
- if (_isJson)
23
- return;
24
- clack.log.step(c.accent.bold(title));
25
- }
26
- /** Annule proprement (prompt annulé) et quitte avec succès. */
27
- export function cancel(msg = 'Annulé') {
28
- if (!_isJson)
29
- clack.cancel(c.yellow(msg));
30
- process.exit(0);
31
- }
32
- export function spin(msg) {
33
- if (_isJson)
34
- return { update: () => { }, stop: () => { }, fail: () => { } };
35
- const s = clack.spinner();
36
- s.start(msg);
37
- return {
38
- update: m => s.message(m),
39
- stop: m => s.stop(m),
40
- fail: m => s.stop(m, 1),
41
- };
42
- }
43
- export const log = {
44
- success: (msg) => { if (!_isJson)
45
- clack.log.success(c.green(msg)); },
46
- error: (msg) => { if (!_isJson)
47
- clack.log.error(c.red(msg)); },
48
- warn: (msg) => { if (!_isJson)
49
- clack.log.warn(c.yellow(msg)); },
50
- info: (msg) => { if (!_isJson)
51
- clack.log.info(c.blue(msg)); },
52
- step: (msg) => { if (!_isJson)
53
- clack.log.step(chalk.dim(msg)); },
54
- dim: (msg) => { if (!_isJson)
55
- clack.log.message(chalk.dim(msg)); },
56
- raw: (msg) => { if (!_isJson)
57
- process.stdout.write(msg + '\n'); },
58
- };
59
- /** Barre de progression inline (0-100). */
60
- export function progress(pct, label) {
61
- if (_isJson)
62
- return;
63
- const clamped = Math.max(0, Math.min(100, pct));
64
- const filled = Math.floor(clamped / 5);
65
- const bar = c.cyan('━'.repeat(filled)) + chalk.dim(''.repeat(20 - filled));
66
- const suffix = label ? ` ${chalk.dim(label)}` : '';
67
- process.stdout.write(`\r ${bar} ${chalk.bold(String(clamped))}%${suffix}`);
68
- if (clamped >= 100)
69
- process.stdout.write('\n');
70
- }
71
- export { clack };
1
+ import * as clack from '@clack/prompts';
2
+ import { c, chalk } from './theme.js';
3
+ const _isJson = process.argv.includes('--json');
4
+ export function isJson() { return _isJson; }
5
+ export function json(data) {
6
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
7
+ process.exit(0);
8
+ }
9
+ export function intro(title) {
10
+ if (_isJson)
11
+ return;
12
+ clack.intro(c.primary.bold(` ${title} `));
13
+ }
14
+ export function outro(msg) {
15
+ if (_isJson)
16
+ return;
17
+ clack.outro(c.accent(msg));
18
+ }
19
+ export function section(title) {
20
+ if (_isJson)
21
+ return;
22
+ clack.log.step(c.accent.bold(title));
23
+ }
24
+ export function cancel(msg = 'Cancelled') {
25
+ if (!_isJson)
26
+ clack.cancel(c.yellow(msg));
27
+ process.exit(0);
28
+ }
29
+ export function spin(msg) {
30
+ if (_isJson)
31
+ return { update: () => { }, stop: () => { }, fail: () => { } };
32
+ const s = clack.spinner();
33
+ s.start(msg);
34
+ return {
35
+ update: m => s.message(m),
36
+ stop: m => s.stop(m),
37
+ fail: m => s.stop(m, 1),
38
+ };
39
+ }
40
+ export const log = {
41
+ success: (msg) => { if (!_isJson)
42
+ clack.log.success(c.green(msg)); },
43
+ error: (msg) => { if (!_isJson)
44
+ clack.log.error(c.red(msg)); },
45
+ warn: (msg) => { if (!_isJson)
46
+ clack.log.warn(c.yellow(msg)); },
47
+ info: (msg) => { if (!_isJson)
48
+ clack.log.info(c.blue(msg)); },
49
+ step: (msg) => { if (!_isJson)
50
+ clack.log.step(chalk.dim(msg)); },
51
+ dim: (msg) => { if (!_isJson)
52
+ clack.log.message(chalk.dim(msg)); },
53
+ raw: (msg) => { if (!_isJson)
54
+ process.stdout.write(msg + '\n'); },
55
+ };
56
+ export function progress(pct, label) {
57
+ if (_isJson)
58
+ return;
59
+ const clamped = Math.max(0, Math.min(100, pct));
60
+ const filled = Math.floor(clamped / 5);
61
+ const bar = c.cyan('━'.repeat(filled)) + chalk.dim('─'.repeat(20 - filled));
62
+ const suffix = label ? ` ${chalk.dim(label)}` : '';
63
+ process.stdout.write(`\r ${bar} ${chalk.bold(String(clamped))}%${suffix}`);
64
+ if (clamped >= 100)
65
+ process.stdout.write('\n');
66
+ }
67
+ export { clack };
@@ -1,30 +1,26 @@
1
- import { text, confirm, select, multiselect, isCancel } from '@clack/prompts';
2
- import { cancel } from './output.js';
3
- /** Prompt texte. Annulation → cancel() propre. */
4
- export async function promptText(opts) {
5
- const res = await text(opts);
6
- if (isCancel(res))
7
- cancel();
8
- return res;
9
- }
10
- /** Prompt oui/non. */
11
- export async function promptConfirm(message, initialValue = false) {
12
- const res = await confirm({ message, initialValue });
13
- if (isCancel(res))
14
- cancel();
15
- return res;
16
- }
17
- /** Prompt sélection simple. */
18
- export async function promptSelect(message, options) {
19
- const res = await select({ message, options: options });
20
- if (isCancel(res))
21
- cancel();
22
- return res;
23
- }
24
- /** Prompt sélection multiple. */
25
- export async function promptMulti(message, options, required = false) {
26
- const res = await multiselect({ message, options: options, required });
27
- if (isCancel(res))
28
- cancel();
29
- return res;
30
- }
1
+ import { text, confirm, select, multiselect, isCancel } from '@clack/prompts';
2
+ import { cancel } from './output.js';
3
+ export async function promptText(opts) {
4
+ const res = await text(opts);
5
+ if (isCancel(res))
6
+ cancel();
7
+ return res;
8
+ }
9
+ export async function promptConfirm(message, initialValue = false) {
10
+ const res = await confirm({ message, initialValue });
11
+ if (isCancel(res))
12
+ cancel();
13
+ return res;
14
+ }
15
+ export async function promptSelect(message, options) {
16
+ const res = await select({ message, options: options });
17
+ if (isCancel(res))
18
+ cancel();
19
+ return res;
20
+ }
21
+ export async function promptMulti(message, options, required = false) {
22
+ const res = await multiselect({ message, options: options, required });
23
+ if (isCancel(res))
24
+ cancel();
25
+ return res;
26
+ }
package/dist/ui/theme.js CHANGED
@@ -1,18 +1,16 @@
1
- import chalk from 'chalk';
2
- /** Palette de couleurs unique du CLI. */
3
- export const c = {
4
- primary: chalk.hex('#FF6B35'),
5
- accent: chalk.hex('#FFB347'),
6
- green: chalk.hex('#4CAF50'),
7
- red: chalk.hex('#E53935'),
8
- yellow: chalk.hex('#FFC107'),
9
- blue: chalk.hex('#6C8EBF'),
10
- cyan: chalk.hex('#4ECDC4'),
11
- dim: chalk.dim,
12
- bold: chalk.bold,
13
- };
14
- /** Désactive/active la couleur globalement (--no-color). */
15
- export function setColorEnabled(on) {
16
- chalk.level = on ? (chalk.level || 1) : 0;
17
- }
18
- export { chalk };
1
+ import chalk from 'chalk';
2
+ export const c = {
3
+ primary: chalk.hex('#FF6B35'),
4
+ accent: chalk.hex('#FFB347'),
5
+ green: chalk.hex('#4CAF50'),
6
+ red: chalk.hex('#E53935'),
7
+ yellow: chalk.hex('#FFC107'),
8
+ blue: chalk.hex('#6C8EBF'),
9
+ cyan: chalk.hex('#4ECDC4'),
10
+ dim: chalk.dim,
11
+ bold: chalk.bold,
12
+ };
13
+ export function setColorEnabled(on) {
14
+ chalk.level = on ? (chalk.level || 1) : 0;
15
+ }
16
+ export { chalk };
package/package.json CHANGED
@@ -1,46 +1,44 @@
1
- {
2
- "name": "@powernukkitx/cli",
3
- "version": "1.0.0",
4
- "description": "PowerNukkitX CLI — Manage your Minecraft Bedrock servers with style",
5
- "keywords": [
6
- "powernukkitx",
7
- "pnx",
8
- "minecraft",
9
- "bedrock",
10
- "server",
11
- "cli"
12
- ],
13
- "license": "MIT",
14
- "author": "AzaleeX",
15
- "type": "module",
16
- "main": "dist/cli.js",
17
- "bin": {
18
- "pnx": "dist/cli.js"
19
- },
20
- "files": [
21
- "dist"
22
- ],
23
- "engines": {
24
- "node": ">=20.12.0"
25
- },
26
- "scripts": {
27
- "build": "tsc && node scripts/copy-assets.mjs",
28
- "dev": "tsc --watch",
29
- "start": "node dist/cli.js",
30
- "test": "node --import tsx --test src/**/*.test.ts",
31
- "prepublishOnly": "npm run build",
32
- "build:sea": "npm run build && node scripts/build-sea.mjs"
33
- },
34
- "dependencies": {
35
- "@clack/prompts": "^1.7.0",
36
- "chalk": "^5.6.2",
37
- "citty": "^0.2.2"
38
- },
39
- "devDependencies": {
40
- "@types/node": "^22.19.19",
41
- "esbuild": "^0.28.0",
42
- "postject": "^1.0.0-alpha.6",
43
- "tsx": "^4.19.2",
44
- "typescript": "^5.9.3"
45
- }
46
- }
1
+ {
2
+ "name": "@powernukkitx/cli",
3
+ "version": "1.0.2",
4
+ "description": "PowerNukkitX CLI — Manage your Minecraft Bedrock servers with style",
5
+ "keywords": [
6
+ "powernukkitx",
7
+ "pnx",
8
+ "minecraft",
9
+ "bedrock",
10
+ "server",
11
+ "cli"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "AzaleeX",
15
+ "type": "module",
16
+ "main": "dist/cli.js",
17
+ "bin": {
18
+ "pnx": "dist/cli.js"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/PowerNukkitX/pnx-cli.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/PowerNukkitX/pnx-cli/issues"
29
+ },
30
+ "homepage": "https://github.com/PowerNukkitX/pnx-cli#readme",
31
+ "engines": {
32
+ "node": ">=20.12.0"
33
+ },
34
+ "scripts": {
35
+ "start": "node dist/cli.js",
36
+ "check": "find dist -name '*.js' -print0 | xargs -0 -n1 node --check",
37
+ "smoke": "node dist/cli.js --help"
38
+ },
39
+ "dependencies": {
40
+ "@clack/prompts": "^1.7.0",
41
+ "chalk": "^5.6.2",
42
+ "citty": "^0.2.2"
43
+ }
44
+ }
package/dist/bundle.js DELETED
@@ -1,18 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __copyProps = (to, from, except, desc) => {
7
- if (from && typeof from === "object" || typeof from === "function") {
8
- for (let key of __getOwnPropNames(from))
9
- if (!__hasOwnProp.call(to, key) && key !== except)
10
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
- }
12
- return to;
13
- };
14
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
-
16
- // src/index.ts
17
- var index_exports = {};
18
- module.exports = __toCommonJS(index_exports);
@@ -1,2 +0,0 @@
1
- import type { CommandDef } from 'citty';
2
- export declare const configCmd: CommandDef;
@@ -1,61 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import chalk from 'chalk';
4
- import { intro, outro, log } from '../core/output.js';
5
- import { resolveServerDir } from '../utils/server.js';
6
- import { t } from '../lang/index.js';
7
- function colorVal(val) {
8
- if (val === 'true' || val === 'on')
9
- return chalk.green(val);
10
- if (val === 'false' || val === 'off')
11
- return chalk.red(val);
12
- if (/^\d+(\.\d+)?$/.test(val))
13
- return chalk.cyan(val);
14
- return chalk.white(val);
15
- }
16
- export const configCmd = {
17
- meta: { description: 'View server configuration' },
18
- args: {},
19
- async run() {
20
- intro(`⚙️ ${t('config.title')}`);
21
- const serverPath = await resolveServerDir();
22
- if (!serverPath) {
23
- log.error(t('config.noServer'));
24
- process.exit(1);
25
- }
26
- let cfgPath = join(serverPath, 'pnx.yml');
27
- if (!existsSync(cfgPath))
28
- cfgPath = join(serverPath, 'server.properties');
29
- if (!existsSync(cfgPath)) {
30
- log.error(t('config.notFound'));
31
- process.exit(1);
32
- }
33
- let content = readFileSync(cfgPath, 'utf-8');
34
- if (content.charCodeAt(0) === 0xFEFF)
35
- content = content.slice(1);
36
- for (const line of content.split('\n')) {
37
- const trimmed = line.trim();
38
- if (!trimmed) {
39
- console.log();
40
- continue;
41
- }
42
- if (trimmed.startsWith('#')) {
43
- console.log(` ${chalk.dim(line)}`);
44
- continue;
45
- }
46
- const sep = cfgPath.endsWith('.yml') ? ':' : '=';
47
- const idx = trimmed.indexOf(sep);
48
- if (idx > 0) {
49
- const key = trimmed.slice(0, idx);
50
- const val = trimmed.slice(idx + 1).trim();
51
- const indent = chalk.dim(line.match(/^(\s*)/)?.[1] ?? '');
52
- console.log(` ${indent}${chalk.hex('#6C8EBF')(key)}${sep}${val ? ' ' + colorVal(val) : ''}`);
53
- }
54
- else {
55
- console.log(` ${chalk.dim(trimmed)}`);
56
- }
57
- }
58
- log.info(t('config.editHint', cfgPath));
59
- outro('');
60
- },
61
- };
@@ -1,2 +0,0 @@
1
- import type { CommandDef } from 'citty';
2
- export declare const langCmd: CommandDef;
@@ -1,28 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1,13 +0,0 @@
1
- export interface PnxConfig {
2
- defaultServer?: string;
3
- lang?: string;
4
- lastUpdateCheck?: number;
5
- lastCoreVersion?: string;
6
- }
7
- export declare function getConfig(): Promise<PnxConfig>;
8
- export declare function setConfig<K extends keyof PnxConfig>(key: K, value: PnxConfig[K]): Promise<void>;
9
- export declare const getDefaultServer: () => Promise<string | null>;
10
- export declare const setDefaultServer: (p: string) => Promise<void>;
11
- export declare const clearDefaultServer: () => Promise<void>;
12
- export declare const getLang: () => Promise<string>;
13
- export declare const setLang: (l: string) => Promise<void>;
@@ -1,31 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
3
- import { homedir } from 'node:os';
4
- import { join } from 'node:path';
5
- const DIR = join(homedir(), '.pnx-cli');
6
- const FILE = join(DIR, 'config.json');
7
- export async function getConfig() {
8
- try {
9
- if (existsSync(FILE))
10
- return JSON.parse(await readFile(FILE, 'utf-8'));
11
- }
12
- catch { }
13
- return {};
14
- }
15
- export async function setConfig(key, value) {
16
- try {
17
- const cfg = await getConfig();
18
- if (value === undefined)
19
- delete cfg[key];
20
- else
21
- cfg[key] = value;
22
- await mkdir(DIR, { recursive: true });
23
- await writeFile(FILE, JSON.stringify(cfg, null, 2), 'utf-8');
24
- }
25
- catch { }
26
- }
27
- export const getDefaultServer = async () => (await getConfig()).defaultServer ?? null;
28
- export const setDefaultServer = (p) => setConfig('defaultServer', p);
29
- export const clearDefaultServer = () => setConfig('defaultServer', undefined);
30
- export const getLang = async () => (await getConfig()).lang ?? 'fr';
31
- export const setLang = (l) => setConfig('lang', l);
@@ -1,13 +0,0 @@
1
- export interface FetchOpts {
2
- retries?: number;
3
- timeout?: number;
4
- headers?: Record<string, string>;
5
- }
6
- export interface DownloadOpts {
7
- label?: string;
8
- onProgress?: (pct: number, downloaded: number, total: number) => void;
9
- timeout?: number;
10
- retries?: number;
11
- }
12
- export declare function fetchJSON(url: string, opts?: FetchOpts): Promise<unknown>;
13
- export declare function downloadFile(url: string, dest: string, opts?: DownloadOpts): Promise<void>;
@@ -1,139 +0,0 @@
1
- import https from 'node:https';
2
- import http from 'node:http';
3
- import { createWriteStream } from 'node:fs';
4
- import { log } from './output.js';
5
- class NetworkError extends Error {
6
- statusCode;
7
- attempt;
8
- constructor(message, statusCode, attempt) {
9
- super(message);
10
- this.statusCode = statusCode;
11
- this.attempt = attempt;
12
- this.name = 'NetworkError';
13
- }
14
- }
15
- function sleep(ms) {
16
- return new Promise((r) => setTimeout(r, ms));
17
- }
18
- function doGet(url, timeoutMs, headers) {
19
- return new Promise((resolve, reject) => {
20
- const mod = url.startsWith('https') ? https : http;
21
- const req = mod.get(url, { timeout: timeoutMs, headers }, (res) => {
22
- let body = '';
23
- res.on('data', (chunk) => (body += chunk.toString()));
24
- res.on('end', () => resolve({
25
- statusCode: res.statusCode ?? 0,
26
- location: res.headers.location,
27
- body,
28
- contentLength: parseInt(res.headers['content-length'] || '0', 10),
29
- raw: res,
30
- }));
31
- res.on('error', reject);
32
- });
33
- req.on('error', reject);
34
- req.on('timeout', function () {
35
- this.destroy();
36
- reject(new NetworkError('Request timed out'));
37
- });
38
- });
39
- }
40
- async function fetchRaw(url, opts, redirects = 5) {
41
- if (redirects <= 0)
42
- throw new NetworkError('Too many redirects');
43
- const res = await doGet(url, opts.timeout, { 'User-Agent': 'pnx-cli', ...opts.headers });
44
- if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.location) {
45
- return fetchRaw(res.location, opts, redirects - 1);
46
- }
47
- if (res.statusCode !== 200)
48
- throw new NetworkError(`HTTP ${res.statusCode}`, res.statusCode);
49
- return res.body;
50
- }
51
- export async function fetchJSON(url, opts) {
52
- const config = {
53
- retries: opts?.retries ?? 3,
54
- timeout: opts?.timeout ?? 15_000,
55
- headers: opts?.headers ?? {},
56
- };
57
- let lastErr = new Error('Unknown error');
58
- for (let attempt = 1; attempt <= config.retries; attempt++) {
59
- try {
60
- const body = await fetchRaw(url, config);
61
- return JSON.parse(body);
62
- }
63
- catch (err) {
64
- lastErr = err;
65
- if (attempt < config.retries) {
66
- log.warn(`Network error (attempt ${attempt}/${config.retries}): ${err.message}`);
67
- await sleep(Math.pow(2, attempt - 1) * 1000);
68
- }
69
- }
70
- }
71
- throw lastErr;
72
- }
73
- function doStream(url, dest, timeoutMs, onData) {
74
- return new Promise((resolve, reject) => {
75
- const mod = url.startsWith('https') ? https : http;
76
- const file = createWriteStream(dest);
77
- const req = mod.get(url, { timeout: timeoutMs, headers: { 'User-Agent': 'pnx-cli' } }, (res) => {
78
- const code = res.statusCode ?? 0;
79
- if ([301, 302, 303, 307, 308].includes(code) && res.headers.location) {
80
- file.close();
81
- resolve(doStream(res.headers.location, dest, timeoutMs, onData));
82
- return;
83
- }
84
- if (code !== 200) {
85
- file.close();
86
- reject(new NetworkError(`HTTP ${code}`, code));
87
- return;
88
- }
89
- const total = parseInt(res.headers['content-length'] || '0', 10);
90
- res.on('data', (chunk) => onData(chunk, total));
91
- res.pipe(file);
92
- file.on('finish', () => {
93
- file.close();
94
- resolve();
95
- });
96
- file.on('error', reject);
97
- });
98
- req.on('error', (err) => {
99
- file.close();
100
- reject(err);
101
- });
102
- req.on('timeout', function () {
103
- this.destroy();
104
- file.close();
105
- reject(new NetworkError('Download timed out'));
106
- });
107
- });
108
- }
109
- export async function downloadFile(url, dest, opts) {
110
- const retries = opts?.retries ?? 3;
111
- const timeout = opts?.timeout ?? 120_000;
112
- const onProgress = opts?.onProgress;
113
- let lastErr = new Error('Unknown error');
114
- for (let attempt = 1; attempt <= retries; attempt++) {
115
- try {
116
- let downloaded = 0;
117
- let lastPct = -1;
118
- await doStream(url, dest, timeout, (chunk, total) => {
119
- downloaded += chunk.length;
120
- if (onProgress && total > 0) {
121
- const pct = Math.round((downloaded / total) * 100);
122
- if (pct !== lastPct && pct % 5 === 0) {
123
- onProgress(pct, downloaded, total);
124
- lastPct = pct;
125
- }
126
- }
127
- });
128
- return;
129
- }
130
- catch (err) {
131
- lastErr = err;
132
- if (attempt < retries) {
133
- log.warn(`Download error (attempt ${attempt}/${retries}): ${err.message}`);
134
- await sleep(Math.pow(2, attempt - 1) * 1000);
135
- }
136
- }
137
- }
138
- throw lastErr;
139
- }