nxcode-ide 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.
- package/README.md +161 -0
- package/bin/nxcode-ide.js +8 -0
- package/package.json +40 -0
- package/screenshot/a1.png +0 -0
- package/src/cli.js +196 -0
- package/src/downloader.js +137 -0
- package/src/launcher.js +23 -0
- package/src/metadata.js +126 -0
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# NxCode IDE
|
|
2
|
+
|
|
3
|
+
Downloader resmi **NxCode Desktop IDE** untuk Windows.
|
|
4
|
+
|
|
5
|
+
NxCode menyatukan project explorer, editor multi-tab, terminal PowerShell,
|
|
6
|
+
Chat AI, dan Database Manager dalam satu aplikasi desktop.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Perbedaan versi
|
|
11
|
+
|
|
12
|
+
Paket npm dan aplikasi IDE memiliki siklus versi yang berbeda:
|
|
13
|
+
|
|
14
|
+
- `nxcode-ide@1.0.0` adalah versi downloader npm.
|
|
15
|
+
- Versi NxCode IDE terbaru dibaca dari
|
|
16
|
+
[nxdom.org/download.json](https://nxdom.org/download.json).
|
|
17
|
+
- Saat ini distribusi resmi menyediakan NxCode IDE `1.0.5`.
|
|
18
|
+
|
|
19
|
+
Karena metadata dibaca secara dinamis, downloader tidak perlu diperbarui setiap
|
|
20
|
+
kali versi IDE baru diterbitkan.
|
|
21
|
+
|
|
22
|
+
## Persyaratan
|
|
23
|
+
|
|
24
|
+
- Windows 10/11 64-bit untuk menjalankan installer
|
|
25
|
+
- Node.js 18 atau lebih baru untuk memakai downloader npm
|
|
26
|
+
- Koneksi internet
|
|
27
|
+
- Ruang kosong untuk installer sekitar 189 MB dan aplikasi yang dipasang
|
|
28
|
+
|
|
29
|
+
## Instalasi downloader
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install --global nxcode-ide
|
|
33
|
+
nxcode-ide --help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Tanpa instalasi global:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx nxcode-ide info
|
|
40
|
+
npx nxcode-ide install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Instalasi paket npm tidak otomatis mengunduh atau menjalankan EXE.
|
|
44
|
+
|
|
45
|
+
## Menginstal NxCode IDE
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
nxcode-ide install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Alurnya:
|
|
52
|
+
|
|
53
|
+
1. Mengambil versi dan URL resmi dari `https://nxdom.org/download.json`.
|
|
54
|
+
2. Mencocokkan asset dengan GitHub Release.
|
|
55
|
+
3. Meminta persetujuan sebelum mengunduh installer.
|
|
56
|
+
4. Menampilkan progres download.
|
|
57
|
+
5. Memeriksa ukuran dan SHA-256 dari GitHub Releases API.
|
|
58
|
+
6. Meminta persetujuan sebelum membuka installer.
|
|
59
|
+
|
|
60
|
+
Untuk otomasi non-interaktif:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
nxcode-ide install --yes
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`--yes` menyetujui download dan pembukaan installer. Gunakan hanya pada
|
|
67
|
+
lingkungan yang Anda kendalikan.
|
|
68
|
+
|
|
69
|
+
## Download tanpa menjalankan installer
|
|
70
|
+
|
|
71
|
+
Simpan installer di direktori aktif:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
nxcode-ide download --yes
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Tentukan lokasi sendiri:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
nxcode-ide download --output "D:\Downloads\NxCode-Setup.exe" --yes
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Command `download` tidak pernah membuka installer. Mode ini juga dapat dipakai
|
|
84
|
+
untuk mengambil installer dari sistem non-Windows.
|
|
85
|
+
|
|
86
|
+
## Melihat informasi rilis
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
nxcode-ide info
|
|
90
|
+
nxcode-ide info --json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Informasi meliputi versi IDE, platform, ukuran, SHA-256, halaman release, dan
|
|
94
|
+
URL installer.
|
|
95
|
+
|
|
96
|
+
## Keamanan distribusi
|
|
97
|
+
|
|
98
|
+
- Hanya URL HTTPS dari asset `github.com/.../releases/download/...` yang diterima.
|
|
99
|
+
- Nama asset harus berupa file `.exe` yang aman.
|
|
100
|
+
- Asset harus tercatat pada GitHub Release yang sesuai.
|
|
101
|
+
- Ukuran file dan digest SHA-256 harus cocok sebelum installer dapat dijalankan.
|
|
102
|
+
- File parsial dibersihkan jika download atau verifikasi gagal.
|
|
103
|
+
- EXE tidak dibundel ke tarball npm; installer tetap berasal dari GitHub Release resmi.
|
|
104
|
+
|
|
105
|
+
Untuk NxCode IDE `1.0.5`, asset resminya adalah
|
|
106
|
+
`Nx-Code-Setup-1.0.5.exe` dengan SHA-256:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
6665009acd430a8e4bf80ab8cbd9666879c25a50a291eb0bbc033cb78cc9c04f
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Download manual
|
|
113
|
+
|
|
114
|
+
Versi terbaru dan tombol download tersedia di:
|
|
115
|
+
|
|
116
|
+
- [NxCode pada nxdom.org](https://nxdom.org/#nxcode)
|
|
117
|
+
- [GitHub Release NxCode](https://github.com/Nxadom/software/releases/latest)
|
|
118
|
+
|
|
119
|
+
## Memperbarui downloader
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm install --global nxcode-ide@latest
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Versi IDE terbaru tetap ditentukan oleh metadata distribusi Nxdom.
|
|
126
|
+
|
|
127
|
+
## Uninstall downloader
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npm uninstall --global nxcode-ide
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Menghapus downloader npm tidak menghapus NxCode IDE yang sudah dipasang melalui
|
|
134
|
+
installer Windows.
|
|
135
|
+
|
|
136
|
+
## Troubleshooting
|
|
137
|
+
|
|
138
|
+
### Command tidak dikenali
|
|
139
|
+
|
|
140
|
+
Tutup dan buka kembali terminal setelah instalasi global, lalu pastikan folder
|
|
141
|
+
global npm tersedia pada `PATH`.
|
|
142
|
+
|
|
143
|
+
### Konfirmasi tidak tersedia
|
|
144
|
+
|
|
145
|
+
Pada CI atau terminal non-interaktif, tambahkan `--yes`.
|
|
146
|
+
|
|
147
|
+
### Verifikasi SHA-256 gagal
|
|
148
|
+
|
|
149
|
+
File tidak akan dijalankan. Hapus hasil download, periksa koneksi atau proxy,
|
|
150
|
+
kemudian coba lagi. Jangan membuka file yang gagal diverifikasi.
|
|
151
|
+
|
|
152
|
+
### Windows menampilkan SmartScreen
|
|
153
|
+
|
|
154
|
+
Pastikan sumber installer adalah release resmi Nxdom dan SHA-256 telah
|
|
155
|
+
diverifikasi oleh downloader sebelum melanjutkan.
|
|
156
|
+
|
|
157
|
+
## Dokumentasi
|
|
158
|
+
|
|
159
|
+
- [Dokumentasi Nxdom](https://nxdom.org/docs)
|
|
160
|
+
- [NxCode IDE](https://nxdom.org/#nxcode)
|
|
161
|
+
- [Laporan masalah](https://github.com/Nxadom/software/issues)
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nxcode-ide",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Downloader resmi NxCode Desktop IDE untuk Windows.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"nxcode-ide": "bin/nxcode-ide.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"screenshot"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node bin/nxcode-ide.js",
|
|
19
|
+
"test": "node --test",
|
|
20
|
+
"prepublishOnly": "npm test && node bin/nxcode-ide.js --help",
|
|
21
|
+
"publish:dry": "npm publish --dry-run"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/Nxadom/software.git"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://nxdom.org/#nxcode",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/Nxadom/software/issues"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"nxcode",
|
|
33
|
+
"nxdom",
|
|
34
|
+
"ide",
|
|
35
|
+
"windows",
|
|
36
|
+
"installer",
|
|
37
|
+
"ai"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|
|
Binary file
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { stdin, stdout } from "node:process";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createProgressReporter,
|
|
8
|
+
defaultInstallerPath,
|
|
9
|
+
downloadInstaller,
|
|
10
|
+
reusableInstaller,
|
|
11
|
+
} from "./downloader.js";
|
|
12
|
+
import { assertWindows, launchInstaller } from "./launcher.js";
|
|
13
|
+
import { fetchInstallerMetadata } from "./metadata.js";
|
|
14
|
+
|
|
15
|
+
const packageJson = JSON.parse(
|
|
16
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const HELP = `NxCode Desktop IDE downloader
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
nxcode-ide install [--yes]
|
|
23
|
+
nxcode-ide download [--output <path>] [--yes]
|
|
24
|
+
nxcode-ide info [--json]
|
|
25
|
+
nxcode-ide --version
|
|
26
|
+
nxcode-ide --help
|
|
27
|
+
|
|
28
|
+
Commands:
|
|
29
|
+
install Download, verify SHA-256, lalu buka installer Windows
|
|
30
|
+
download Download dan verifikasi installer tanpa menjalankannya
|
|
31
|
+
info Tampilkan versi serta sumber installer terbaru
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
-y, --yes Setujui prompt non-interaktif
|
|
35
|
+
-o, --output PATH Lokasi file untuk command download
|
|
36
|
+
--json Output JSON untuk command info
|
|
37
|
+
-v, --version Tampilkan versi paket downloader
|
|
38
|
+
-h, --help Tampilkan bantuan
|
|
39
|
+
|
|
40
|
+
Download resmi: https://nxdom.org/#nxcode
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
function parseArgs(args) {
|
|
44
|
+
const options = { yes: false, json: false, output: null };
|
|
45
|
+
let command = null;
|
|
46
|
+
|
|
47
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
48
|
+
const arg = args[index];
|
|
49
|
+
if (arg === "-h" || arg === "--help") return { command: "help", options };
|
|
50
|
+
if (arg === "-v" || arg === "--version") {
|
|
51
|
+
return { command: "version", options };
|
|
52
|
+
}
|
|
53
|
+
if (arg === "-y" || arg === "--yes") {
|
|
54
|
+
options.yes = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (arg === "--json") {
|
|
58
|
+
options.json = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (arg === "-o" || arg === "--output") {
|
|
62
|
+
const value = args[index + 1];
|
|
63
|
+
if (!value || value.startsWith("-")) {
|
|
64
|
+
throw new Error(`${arg} membutuhkan path tujuan.`);
|
|
65
|
+
}
|
|
66
|
+
options.output = value;
|
|
67
|
+
index += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg.startsWith("--output=")) {
|
|
71
|
+
options.output = arg.slice("--output=".length);
|
|
72
|
+
if (!options.output) throw new Error("--output membutuhkan path tujuan.");
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg.startsWith("-")) throw new Error(`Opsi tidak dikenal: ${arg}`);
|
|
76
|
+
if (command) throw new Error(`Argumen tidak dikenal: ${arg}`);
|
|
77
|
+
command = arg;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { command: command ?? "help", options };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function terminalConfirm(message, { input = stdin, output = stdout } = {}) {
|
|
84
|
+
if (!input.isTTY) {
|
|
85
|
+
throw new Error("Konfirmasi interaktif tidak tersedia. Gunakan --yes.");
|
|
86
|
+
}
|
|
87
|
+
const prompt = createInterface({ input, output });
|
|
88
|
+
try {
|
|
89
|
+
const answer = await prompt.question(`${message} [y/N] `);
|
|
90
|
+
return /^(y|yes|ya)$/i.test(answer.trim());
|
|
91
|
+
} finally {
|
|
92
|
+
prompt.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatMegabytes(bytes) {
|
|
97
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function printMetadata(metadata, output) {
|
|
101
|
+
output.write(`NxCode IDE ${metadata.version}\n`);
|
|
102
|
+
output.write(`${metadata.label || "Windows 64-bit"}\n`);
|
|
103
|
+
output.write(`Ukuran: ${formatMegabytes(metadata.size)}\n`);
|
|
104
|
+
output.write(`SHA-256: ${metadata.sha256}\n`);
|
|
105
|
+
output.write(`Release: ${metadata.releaseUrl}\n`);
|
|
106
|
+
output.write(`Installer: ${metadata.url}\n`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function runCli(
|
|
110
|
+
args,
|
|
111
|
+
{
|
|
112
|
+
output = stdout,
|
|
113
|
+
input = stdin,
|
|
114
|
+
platform = process.platform,
|
|
115
|
+
cwd = process.cwd(),
|
|
116
|
+
getMetadata = fetchInstallerMetadata,
|
|
117
|
+
download = downloadInstaller,
|
|
118
|
+
isReusable = reusableInstaller,
|
|
119
|
+
launch = launchInstaller,
|
|
120
|
+
confirm = (message) => terminalConfirm(message, { input, output }),
|
|
121
|
+
} = {},
|
|
122
|
+
) {
|
|
123
|
+
const { command, options } = parseArgs(args);
|
|
124
|
+
|
|
125
|
+
if (command === "help") {
|
|
126
|
+
output.write(HELP);
|
|
127
|
+
return { command };
|
|
128
|
+
}
|
|
129
|
+
if (command === "version") {
|
|
130
|
+
output.write(`${packageJson.version}\n`);
|
|
131
|
+
return { command, version: packageJson.version };
|
|
132
|
+
}
|
|
133
|
+
if (!["install", "download", "info"].includes(command)) {
|
|
134
|
+
throw new Error(`Command tidak dikenal: ${command}. Gunakan --help.`);
|
|
135
|
+
}
|
|
136
|
+
if (options.output && command !== "download") {
|
|
137
|
+
throw new Error("--output hanya dapat digunakan dengan command download.");
|
|
138
|
+
}
|
|
139
|
+
if (options.json && command !== "info") {
|
|
140
|
+
throw new Error("--json hanya dapat digunakan dengan command info.");
|
|
141
|
+
}
|
|
142
|
+
if (command === "install") assertWindows(platform);
|
|
143
|
+
|
|
144
|
+
const metadata = await getMetadata();
|
|
145
|
+
if (command === "info") {
|
|
146
|
+
if (options.json) output.write(`${JSON.stringify(metadata, null, 2)}\n`);
|
|
147
|
+
else printMetadata(metadata, output);
|
|
148
|
+
return { command, metadata };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const destination =
|
|
152
|
+
command === "install"
|
|
153
|
+
? defaultInstallerPath(metadata)
|
|
154
|
+
: resolve(cwd, options.output ?? metadata.assetName);
|
|
155
|
+
let cached = await isReusable(destination, metadata);
|
|
156
|
+
|
|
157
|
+
if (!cached && !options.yes) {
|
|
158
|
+
const accepted = await confirm(
|
|
159
|
+
`Unduh NxCode IDE ${metadata.version} (${formatMegabytes(metadata.size)})?`,
|
|
160
|
+
);
|
|
161
|
+
if (!accepted) {
|
|
162
|
+
output.write("Download dibatalkan.\n");
|
|
163
|
+
return { command, cancelled: true };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!cached) {
|
|
168
|
+
const progress = createProgressReporter(output);
|
|
169
|
+
await download(metadata, {
|
|
170
|
+
destination,
|
|
171
|
+
onProgress: progress,
|
|
172
|
+
});
|
|
173
|
+
if (output.isTTY) output.write("\n");
|
|
174
|
+
} else {
|
|
175
|
+
output.write("Installer terverifikasi ditemukan di cache.\n");
|
|
176
|
+
}
|
|
177
|
+
output.write(`Installer terverifikasi: ${destination}\n`);
|
|
178
|
+
|
|
179
|
+
if (command === "download") {
|
|
180
|
+
return { command, path: destination, metadata, cached };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!options.yes) {
|
|
184
|
+
const accepted = await confirm("Buka installer NxCode IDE sekarang?");
|
|
185
|
+
if (!accepted) {
|
|
186
|
+
output.write("Installer tidak dibuka. File tetap tersimpan di cache.\n");
|
|
187
|
+
return { command, path: destination, launched: false };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
launch(destination, { platform });
|
|
192
|
+
output.write("Installer NxCode IDE dibuka.\n");
|
|
193
|
+
return { command, path: destination, launched: true };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export { HELP, parseArgs };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
mkdir,
|
|
5
|
+
rename,
|
|
6
|
+
rm,
|
|
7
|
+
stat,
|
|
8
|
+
} from "node:fs/promises";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
11
|
+
import { Readable, Transform } from "node:stream";
|
|
12
|
+
import { pipeline } from "node:stream/promises";
|
|
13
|
+
|
|
14
|
+
function formatBytes(value) {
|
|
15
|
+
const bytes = Math.max(0, Number(value) || 0);
|
|
16
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
17
|
+
const units = ["KB", "MB", "GB"];
|
|
18
|
+
let amount = bytes;
|
|
19
|
+
let index = -1;
|
|
20
|
+
do {
|
|
21
|
+
amount /= 1024;
|
|
22
|
+
index += 1;
|
|
23
|
+
} while (amount >= 1024 && index < units.length - 1);
|
|
24
|
+
return `${amount.toFixed(amount >= 10 ? 1 : 2)} ${units[index]}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function defaultInstallerPath(metadata) {
|
|
28
|
+
return join(tmpdir(), "nxcode-ide", metadata.assetName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createProgressReporter(output = process.stdout) {
|
|
32
|
+
let lastPercent = -1;
|
|
33
|
+
return ({ downloaded, total }) => {
|
|
34
|
+
const percent = total > 0 ? Math.min(100, Math.floor((downloaded / total) * 100)) : 0;
|
|
35
|
+
if (percent === lastPercent && downloaded !== total) return;
|
|
36
|
+
lastPercent = percent;
|
|
37
|
+
const line =
|
|
38
|
+
`Mengunduh ${String(percent).padStart(3)}% · ` +
|
|
39
|
+
`${formatBytes(downloaded)} / ${formatBytes(total)}`;
|
|
40
|
+
if (output?.isTTY) output.write(`\r\x1b[2K${line}`);
|
|
41
|
+
else if (percent === 100 || percent % 10 === 0) output.write(`${line}\n`);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function verifyInstaller(path, metadata) {
|
|
46
|
+
const file = await import("node:fs").then(({ createReadStream }) =>
|
|
47
|
+
createReadStream(path),
|
|
48
|
+
);
|
|
49
|
+
const hash = createHash("sha256");
|
|
50
|
+
let size = 0;
|
|
51
|
+
for await (const chunk of file) {
|
|
52
|
+
hash.update(chunk);
|
|
53
|
+
size += chunk.length;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
ok: size === metadata.size && hash.digest("hex") === metadata.sha256,
|
|
57
|
+
size,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function downloadInstaller(
|
|
62
|
+
metadata,
|
|
63
|
+
{
|
|
64
|
+
fetchImpl = globalThis.fetch,
|
|
65
|
+
destination = defaultInstallerPath(metadata),
|
|
66
|
+
onProgress = createProgressReporter(),
|
|
67
|
+
} = {},
|
|
68
|
+
) {
|
|
69
|
+
const target = resolve(destination);
|
|
70
|
+
if (basename(target).toLowerCase().endsWith(".exe") === false) {
|
|
71
|
+
throw new Error("Tujuan download harus berupa file .exe.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await mkdir(dirname(target), { recursive: true });
|
|
75
|
+
const partial = `${target}.${process.pid}.${Date.now()}.part`;
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await fetchImpl(metadata.url, {
|
|
79
|
+
redirect: "follow",
|
|
80
|
+
headers: { "user-agent": "nxcode-ide-npm" },
|
|
81
|
+
});
|
|
82
|
+
if (!response?.ok || !response.body) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Download installer gagal (HTTP ${response?.status ?? "unknown"}).`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const hash = createHash("sha256");
|
|
89
|
+
let downloaded = 0;
|
|
90
|
+
const source = Readable.fromWeb(response.body);
|
|
91
|
+
const output = createWriteStream(partial, { flags: "wx" });
|
|
92
|
+
const meter = new Transform({
|
|
93
|
+
transform(chunk, _encoding, callback) {
|
|
94
|
+
downloaded += chunk.length;
|
|
95
|
+
if (downloaded > metadata.size) {
|
|
96
|
+
callback(
|
|
97
|
+
new Error("Ukuran installer melebihi metadata GitHub Release."),
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
hash.update(chunk);
|
|
102
|
+
onProgress?.({ downloaded, total: metadata.size });
|
|
103
|
+
callback(null, chunk);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await pipeline(source, meter, output);
|
|
108
|
+
|
|
109
|
+
if (downloaded !== metadata.size) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Ukuran installer tidak cocok: ${downloaded} dari ${metadata.size} byte.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const digest = hash.digest("hex");
|
|
115
|
+
if (digest !== metadata.sha256) {
|
|
116
|
+
throw new Error("Verifikasi SHA-256 installer gagal.");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await rm(target, { force: true });
|
|
120
|
+
await rename(partial, target);
|
|
121
|
+
onProgress?.({ downloaded, total: metadata.size });
|
|
122
|
+
return target;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
await rm(partial, { force: true }).catch(() => {});
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function reusableInstaller(path, metadata) {
|
|
130
|
+
try {
|
|
131
|
+
const info = await stat(path);
|
|
132
|
+
if (!info.isFile() || info.size !== metadata.size) return false;
|
|
133
|
+
return (await verifyInstaller(path, metadata)).ok;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/launcher.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function assertWindows(platform = process.platform) {
|
|
4
|
+
if (platform !== "win32") {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"NxCode IDE saat ini hanya tersedia untuk Windows 10/11 64-bit.",
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function launchInstaller(
|
|
12
|
+
installerPath,
|
|
13
|
+
{ platform = process.platform, spawnImpl = spawn } = {},
|
|
14
|
+
) {
|
|
15
|
+
assertWindows(platform);
|
|
16
|
+
const child = spawnImpl(installerPath, [], {
|
|
17
|
+
detached: true,
|
|
18
|
+
stdio: "ignore",
|
|
19
|
+
windowsHide: false,
|
|
20
|
+
});
|
|
21
|
+
child.unref?.();
|
|
22
|
+
return child;
|
|
23
|
+
}
|
package/src/metadata.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export const DOWNLOAD_METADATA_URL = "https://nxdom.org/download.json";
|
|
2
|
+
|
|
3
|
+
const ALLOWED_DOWNLOAD_HOST = "github.com";
|
|
4
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
5
|
+
const RELEASE_PATH_PATTERN =
|
|
6
|
+
/^\/([^/]+)\/([^/]+)\/releases\/download\/([^/]+)\/([^/]+)$/;
|
|
7
|
+
|
|
8
|
+
function assertResponse(response, label) {
|
|
9
|
+
if (!response?.ok) {
|
|
10
|
+
throw new Error(`${label} gagal (HTTP ${response?.status ?? "unknown"}).`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseReleaseUrl(value) {
|
|
15
|
+
let url;
|
|
16
|
+
try {
|
|
17
|
+
url = new URL(value);
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error("URL installer pada metadata tidak valid.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (url.protocol !== "https:" || url.hostname !== ALLOWED_DOWNLOAD_HOST) {
|
|
23
|
+
throw new Error("URL installer harus memakai HTTPS dari github.com.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const match = url.pathname.match(RELEASE_PATH_PATTERN);
|
|
27
|
+
if (!match) {
|
|
28
|
+
throw new Error("URL installer bukan asset GitHub Release yang valid.");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const [, owner, repo, tag, encodedAssetName] = match;
|
|
32
|
+
const assetName = decodeURIComponent(encodedAssetName);
|
|
33
|
+
if (
|
|
34
|
+
!assetName ||
|
|
35
|
+
assetName === "." ||
|
|
36
|
+
assetName === ".." ||
|
|
37
|
+
assetName.includes("/") ||
|
|
38
|
+
assetName.includes("\\") ||
|
|
39
|
+
!assetName.toLowerCase().endsWith(".exe")
|
|
40
|
+
) {
|
|
41
|
+
throw new Error("Nama asset installer tidak aman.");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
url: url.href,
|
|
45
|
+
owner,
|
|
46
|
+
repo,
|
|
47
|
+
tag: decodeURIComponent(tag),
|
|
48
|
+
assetName,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function validateDownloadMetadata(value) {
|
|
53
|
+
if (!value || typeof value !== "object") {
|
|
54
|
+
throw new Error("Format metadata download tidak valid.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const version = String(value.version ?? "").trim();
|
|
58
|
+
const label = String(value.label ?? "").trim();
|
|
59
|
+
if (!VERSION_PATTERN.test(version)) {
|
|
60
|
+
throw new Error("Versi NxCode pada metadata tidak valid.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const release = parseReleaseUrl(value.url);
|
|
64
|
+
if (release.tag !== `v${version}` && release.tag !== version) {
|
|
65
|
+
throw new Error("Versi metadata tidak cocok dengan tag GitHub Release.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { version, label, ...release };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function fetchInstallerMetadata({
|
|
72
|
+
fetchImpl = globalThis.fetch,
|
|
73
|
+
metadataUrl = DOWNLOAD_METADATA_URL,
|
|
74
|
+
} = {}) {
|
|
75
|
+
if (typeof fetchImpl !== "function") {
|
|
76
|
+
throw new Error("Runtime Node.js ini tidak menyediakan fetch.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const metadataResponse = await fetchImpl(metadataUrl, {
|
|
80
|
+
headers: { accept: "application/json" },
|
|
81
|
+
});
|
|
82
|
+
assertResponse(metadataResponse, "Pengambilan metadata NxCode");
|
|
83
|
+
const metadata = validateDownloadMetadata(await metadataResponse.json());
|
|
84
|
+
|
|
85
|
+
const releaseApiUrl =
|
|
86
|
+
`https://api.github.com/repos/${encodeURIComponent(metadata.owner)}/` +
|
|
87
|
+
`${encodeURIComponent(metadata.repo)}/releases/tags/${encodeURIComponent(metadata.tag)}`;
|
|
88
|
+
const releaseResponse = await fetchImpl(releaseApiUrl, {
|
|
89
|
+
headers: {
|
|
90
|
+
accept: "application/vnd.github+json",
|
|
91
|
+
"user-agent": "nxcode-ide-npm",
|
|
92
|
+
"x-github-api-version": "2022-11-28",
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
assertResponse(releaseResponse, "Pengambilan data GitHub Release");
|
|
96
|
+
|
|
97
|
+
const release = await releaseResponse.json();
|
|
98
|
+
const asset = Array.isArray(release?.assets)
|
|
99
|
+
? release.assets.find(
|
|
100
|
+
(entry) =>
|
|
101
|
+
entry?.name === metadata.assetName &&
|
|
102
|
+
entry?.browser_download_url === metadata.url,
|
|
103
|
+
)
|
|
104
|
+
: null;
|
|
105
|
+
if (!asset) {
|
|
106
|
+
throw new Error("Asset installer tidak ditemukan pada GitHub Release.");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const digest = String(asset.digest ?? "");
|
|
110
|
+
const digestMatch = digest.match(/^sha256:([a-f0-9]{64})$/i);
|
|
111
|
+
if (!digestMatch) {
|
|
112
|
+
throw new Error("GitHub Release belum menyediakan digest SHA-256.");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const size = Number(asset.size);
|
|
116
|
+
if (!Number.isSafeInteger(size) || size <= 0) {
|
|
117
|
+
throw new Error("Ukuran asset GitHub Release tidak valid.");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
...metadata,
|
|
122
|
+
size,
|
|
123
|
+
sha256: digestMatch[1].toLowerCase(),
|
|
124
|
+
releaseUrl: String(release.html_url ?? ""),
|
|
125
|
+
};
|
|
126
|
+
}
|