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 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
+ ![Tampilan NxCode IDE](https://unpkg.com/nxcode-ide@latest/screenshot/a1.png)
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)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from "../src/cli.js";
4
+
5
+ runCli(process.argv.slice(2)).catch((error) => {
6
+ console.error(`Error: ${error.message}`);
7
+ process.exitCode = 1;
8
+ });
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
+ }
@@ -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
+ }
@@ -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
+ }