@onreza/nrz 0.36.1 → 0.36.3
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 +13 -2
- package/package.json +1 -1
- package/scripts/postinstall.js +118 -27
package/README.md
CHANGED
|
@@ -33,14 +33,25 @@ nrz upgrade --channel beta
|
|
|
33
33
|
|
|
34
34
|
**Linux/macOS:**
|
|
35
35
|
```bash
|
|
36
|
-
|
|
36
|
+
installer="$(mktemp)"
|
|
37
|
+
curl -fsSL https://raw.githubusercontent.com/onreza/nrz-cli/main/install.sh -o "$installer"
|
|
38
|
+
less "$installer" # optional: review before running
|
|
39
|
+
bash "$installer"
|
|
40
|
+
rm -f "$installer"
|
|
37
41
|
```
|
|
38
42
|
|
|
39
43
|
**Windows (PowerShell 7+):**
|
|
40
44
|
```powershell
|
|
41
|
-
|
|
45
|
+
$installer = (New-TemporaryFile).FullName
|
|
46
|
+
iwr -useb https://raw.githubusercontent.com/onreza/nrz-cli/main/install.ps1 -OutFile $installer
|
|
47
|
+
Get-Content $installer # optional: review before running
|
|
48
|
+
pwsh -NoProfile -File $installer
|
|
49
|
+
Remove-Item $installer
|
|
42
50
|
```
|
|
43
51
|
|
|
52
|
+
Both installers verify the downloaded archive against the SHA-256 manifest
|
|
53
|
+
published with the selected GitHub release.
|
|
54
|
+
|
|
44
55
|
## Documentation
|
|
45
56
|
|
|
46
57
|
Full documentation and source code: [github.com/onreza/nrz-cli](https://github.com/onreza/nrz-cli)
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { platform, arch } from "node:process";
|
|
3
3
|
import https from "node:https";
|
|
4
|
-
import
|
|
5
|
-
import { chmodSync, mkdirSync, renameSync,
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, renameSync, rmSync } from "node:fs";
|
|
6
6
|
import { join, dirname } from "node:path";
|
|
7
7
|
import { createGunzip } from "node:zlib";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -13,19 +13,26 @@ const __dirname = dirname(__filename);
|
|
|
13
13
|
|
|
14
14
|
const MANIFEST = {
|
|
15
15
|
"assets": {
|
|
16
|
-
"linux-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.
|
|
17
|
-
"darwin-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.
|
|
18
|
-
"darwin-arm64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.
|
|
19
|
-
"win32-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.
|
|
16
|
+
"linux-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.3/nrz-linux-x64.tar.gz",
|
|
17
|
+
"darwin-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.3/nrz-darwin-x64.tar.gz",
|
|
18
|
+
"darwin-arm64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.3/nrz-darwin-arm64.tar.gz",
|
|
19
|
+
"win32-x64": "https://github.com/ONREZA/nrz-cli/releases/download/v0.36.3/nrz-win32-x64.tar.gz"
|
|
20
|
+
},
|
|
21
|
+
"checksums": {
|
|
22
|
+
"linux-x64": "8d0354827b77c7aed5af158c9c481340ae4bb7750f1f93306f6c6112f5856beb",
|
|
23
|
+
"darwin-x64": "54d8edfb035a0921790330257699ffd8b4b9097d9ebbdd1fc1afc3feee927eb0",
|
|
24
|
+
"darwin-arm64": "54b1ef47f7fe83d4f74f2f6e55d2adcd6d5f88c8c65f35289e7cb2de90faf3b2",
|
|
25
|
+
"win32-x64": "5d1abea64dad0df10b14b148d2e902212697bbf064180ad135e870999aeb38da"
|
|
20
26
|
},
|
|
21
27
|
"binName": "nrz",
|
|
22
|
-
"version": "0.36.
|
|
28
|
+
"version": "0.36.3"
|
|
23
29
|
};
|
|
24
30
|
|
|
25
31
|
const platformKey = `${platform}-${arch}`;
|
|
26
32
|
const downloadUrl = MANIFEST.assets[platformKey];
|
|
33
|
+
const expectedSha256 = MANIFEST.checksums[platformKey];
|
|
27
34
|
|
|
28
|
-
if (!downloadUrl) {
|
|
35
|
+
if (!downloadUrl || !expectedSha256) {
|
|
29
36
|
console.error(`No binary available for ${platformKey}`);
|
|
30
37
|
console.error(`Supported platforms: ${Object.keys(MANIFEST.assets).join(", ")}`);
|
|
31
38
|
process.exit(1);
|
|
@@ -33,36 +40,115 @@ if (!downloadUrl) {
|
|
|
33
40
|
|
|
34
41
|
const binDir = join(__dirname, "..", "bin");
|
|
35
42
|
mkdirSync(binDir, { recursive: true });
|
|
43
|
+
const stagingDir = mkdtempSync(join(binDir, ".nrz-install-"));
|
|
44
|
+
const sourceName = MANIFEST.sourceBinName || MANIFEST.binName;
|
|
45
|
+
const ext = platform === "win32" ? ".exe" : "";
|
|
46
|
+
const sourcePath = join(stagingDir, sourceName + ext);
|
|
47
|
+
const targetPath = join(binDir, MANIFEST.binName + ext);
|
|
48
|
+
const backupPath = join(binDir, `.${MANIFEST.binName}${ext}.old-${randomUUID()}`);
|
|
49
|
+
|
|
50
|
+
function replaceInstalledBinary(candidatePath, targetPath, backupPath) {
|
|
51
|
+
if (platform !== "win32" || !existsSync(targetPath)) {
|
|
52
|
+
renameSync(candidatePath, targetPath);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const targetStat = lstatSync(targetPath);
|
|
57
|
+
if (!targetStat.isFile() && !targetStat.isSymbolicLink()) {
|
|
58
|
+
throw new Error(`Refusing to replace non-file binary path: ${targetPath}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
renameSync(targetPath, backupPath);
|
|
62
|
+
try {
|
|
63
|
+
renameSync(candidatePath, targetPath);
|
|
64
|
+
} catch (installError) {
|
|
65
|
+
try {
|
|
66
|
+
renameSync(backupPath, targetPath);
|
|
67
|
+
} catch (restoreError) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Failed to install replacement (${installError.message}) and restore previous binary (${restoreError.message}); previous binary remains at ${backupPath}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Failed to install replacement; previous binary was restored: ${installError.message}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
rmSync(backupPath, { force: true });
|
|
79
|
+
} catch (cleanupError) {
|
|
80
|
+
console.warn(
|
|
81
|
+
`Installed new binary but could not remove backup ${backupPath}: ${cleanupError.message}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
36
85
|
|
|
37
|
-
function download(url, redirectCount = 0) {
|
|
86
|
+
function download(url, expectedSha256, redirectCount = 0) {
|
|
38
87
|
if (redirectCount > 5) {
|
|
39
88
|
return Promise.reject(new Error("Too many redirects"));
|
|
40
89
|
}
|
|
41
90
|
|
|
42
91
|
return new Promise((resolve, reject) => {
|
|
43
|
-
const
|
|
92
|
+
const parsed = new URL(url);
|
|
93
|
+
if (parsed.protocol !== "https:") {
|
|
94
|
+
reject(new Error(`Refusing non-HTTPS download URL: ${url}`));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
44
97
|
|
|
45
|
-
const request =
|
|
98
|
+
const request = https.get(url, (res) => {
|
|
46
99
|
if ([301, 302, 307, 308].includes(res.statusCode)) {
|
|
47
100
|
const location = res.headers.location;
|
|
48
101
|
if (!location) {
|
|
49
102
|
reject(new Error("Redirect without location header"));
|
|
50
103
|
return;
|
|
51
104
|
}
|
|
52
|
-
|
|
105
|
+
res.resume();
|
|
106
|
+
download(new URL(location, url).toString(), expectedSha256, redirectCount + 1)
|
|
107
|
+
.then(resolve)
|
|
108
|
+
.catch(reject);
|
|
53
109
|
return;
|
|
54
110
|
}
|
|
55
111
|
|
|
56
112
|
if (res.statusCode !== 200) {
|
|
113
|
+
res.resume();
|
|
57
114
|
reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
58
115
|
return;
|
|
59
116
|
}
|
|
117
|
+
const contentLength = Number(res.headers["content-length"]);
|
|
118
|
+
if (Number.isFinite(contentLength) && contentLength > 256 * 1024 * 1024) {
|
|
119
|
+
res.resume();
|
|
120
|
+
reject(new Error("Download exceeds 256 MiB"));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
60
123
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
|
|
124
|
+
const chunks = [];
|
|
125
|
+
let received = 0;
|
|
126
|
+
res.on("data", (chunk) => {
|
|
127
|
+
received += chunk.length;
|
|
128
|
+
if (received > 256 * 1024 * 1024) {
|
|
129
|
+
request.destroy(new Error("Download exceeds 256 MiB"));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
chunks.push(chunk);
|
|
133
|
+
});
|
|
134
|
+
res.on("end", () => {
|
|
135
|
+
const archive = Buffer.concat(chunks);
|
|
136
|
+
const actualSha256 = createHash("sha256").update(archive).digest("hex");
|
|
137
|
+
if (actualSha256 !== expectedSha256) {
|
|
138
|
+
reject(new Error(
|
|
139
|
+
`SHA-256 mismatch: expected ${expectedSha256}, received ${actualSha256}`,
|
|
140
|
+
));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const gunzip = createGunzip();
|
|
144
|
+
const extract = tar.extract({ cwd: stagingDir, strip: 1 });
|
|
145
|
+
gunzip.on("error", reject);
|
|
146
|
+
extract.on("error", reject);
|
|
147
|
+
extract.on("finish", resolve);
|
|
148
|
+
gunzip.pipe(extract);
|
|
149
|
+
gunzip.end(archive);
|
|
150
|
+
});
|
|
151
|
+
res.on("error", reject);
|
|
66
152
|
});
|
|
67
153
|
|
|
68
154
|
request.on("error", reject);
|
|
@@ -75,27 +161,32 @@ function download(url, redirectCount = 0) {
|
|
|
75
161
|
|
|
76
162
|
console.log(`Installing ${MANIFEST.binName} for ${platformKey}...`);
|
|
77
163
|
|
|
78
|
-
download(downloadUrl)
|
|
164
|
+
download(downloadUrl, expectedSha256)
|
|
79
165
|
.then(() => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const sourcePath = join(binDir, sourceName + ext);
|
|
83
|
-
const targetPath = join(binDir, MANIFEST.binName + ext);
|
|
84
|
-
|
|
85
|
-
if (sourceName !== MANIFEST.binName && existsSync(sourcePath)) {
|
|
86
|
-
renameSync(sourcePath, targetPath);
|
|
166
|
+
if (!existsSync(sourcePath)) {
|
|
167
|
+
throw new Error(`Archive did not contain ${MANIFEST.binName}${ext}`);
|
|
87
168
|
}
|
|
88
169
|
|
|
89
170
|
if (platform !== "win32") {
|
|
90
171
|
try {
|
|
91
|
-
chmodSync(
|
|
172
|
+
chmodSync(sourcePath, 0o755);
|
|
92
173
|
} catch {
|
|
93
174
|
}
|
|
94
175
|
}
|
|
176
|
+
replaceInstalledBinary(sourcePath, targetPath, backupPath);
|
|
95
177
|
console.log(`Installed ${MANIFEST.binName} v${MANIFEST.version}`);
|
|
96
178
|
})
|
|
97
179
|
.catch((err) => {
|
|
98
180
|
console.error(`Failed to install binary: ${err.message}`);
|
|
99
181
|
console.error(`URL: ${downloadUrl}`);
|
|
100
|
-
process.
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
})
|
|
184
|
+
.finally(() => {
|
|
185
|
+
try {
|
|
186
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
187
|
+
} catch (cleanupError) {
|
|
188
|
+
console.warn(
|
|
189
|
+
`Could not remove staging directory ${stagingDir}: ${cleanupError.message}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
101
192
|
});
|