ztnet-cli 0.1.12

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/LICENSE ADDED
@@ -0,0 +1,8 @@
1
+ GNU Affero General Public License v3.0 (AGPL-3.0-only)
2
+
3
+ This npm package is distributed under the same license as the upstream project:
4
+ https://github.com/JKamsker/ztnet-cli
5
+
6
+ You can find the full license text at:
7
+ https://www.gnu.org/licenses/agpl-3.0.txt
8
+
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # ztnet-cli (npm)
2
+
3
+ This package installs the prebuilt **ZTNet CLI** (`ztnet`) for your platform.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g ztnet-cli
9
+ ztnet --help
10
+ ```
11
+
12
+ ## How it works
13
+
14
+ On install, `ztnet-cli` downloads the matching `ztnet` binary from the GitHub Releases for the same version.
15
+
16
+ Supported platforms:
17
+
18
+ - Windows (x64)
19
+ - Linux (x64)
20
+ - macOS (arm64, x64)
21
+
@@ -0,0 +1 @@
1
+ 51ae0a72def91ab25aace355f44fd8c6feb34a848f8630eb307c9204e1f2fe7a ztnet-0.1.12-aarch64-apple-darwin.tar.gz
@@ -0,0 +1 @@
1
+ 7c6fd2c3c4fcc2cafd40f20b14ded78f8c31bbf4ae195d1b07562b9c435d0017 ztnet-0.1.12-x86_64-pc-windows-msvc.zip
@@ -0,0 +1 @@
1
+ 3138263312e945b64f0b72c75f817cc74909aba551bcfabfa42d10502cc4977f ztnet-0.1.12-x86_64-unknown-linux-gnu.tar.gz
package/bin/ztnet.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ const childProcess = require("node:child_process");
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+
7
+ const packageRoot = path.join(__dirname, "..");
8
+ const vendorDir = path.join(packageRoot, "vendor");
9
+ const exeName = process.platform === "win32" ? "ztnet.exe" : "ztnet";
10
+ const exePath = path.join(vendorDir, exeName);
11
+
12
+ if (!fs.existsSync(exePath)) {
13
+ console.error(`[ztnet-cli] Missing native binary: ${exePath}`);
14
+ console.error(
15
+ "[ztnet-cli] Reinstall to re-run the downloader: npm install -g ztnet-cli",
16
+ );
17
+ process.exit(1);
18
+ }
19
+
20
+ const result = childProcess.spawnSync(exePath, process.argv.slice(2), {
21
+ stdio: "inherit",
22
+ });
23
+
24
+ if (result.error) {
25
+ console.error(`[ztnet-cli] Failed to run ${exePath}: ${result.error.message}`);
26
+ process.exit(1);
27
+ }
28
+
29
+ process.exit(result.status ?? 1);
30
+
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "ztnet-cli",
3
+ "version": "0.1.12",
4
+ "description": "ZTNet CLI \u2014 manage ZeroTier networks via ZTNet",
5
+ "license": "AGPL-3.0-only",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/JKamsker/ztnet-cli.git"
9
+ },
10
+ "homepage": "https://github.com/JKamsker/ztnet-cli",
11
+ "bugs": {
12
+ "url": "https://github.com/JKamsker/ztnet-cli/issues"
13
+ },
14
+ "bin": {
15
+ "ztnet": "bin/ztnet.js"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "artifacts/",
20
+ "scripts/",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "scripts": {
28
+ "postinstall": "node scripts/install.js"
29
+ },
30
+ "dependencies": {
31
+ "extract-zip": "^2.0.1",
32
+ "tar": "^6.2.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
@@ -0,0 +1,235 @@
1
+ const crypto = require("node:crypto");
2
+ const fs = require("node:fs");
3
+ const https = require("node:https");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+
7
+ const extractZip = require("extract-zip");
8
+ const tar = require("tar");
9
+
10
+ function resolvePlatformTarget() {
11
+ const platform = process.platform;
12
+ const arch = process.arch;
13
+
14
+ if (platform === "win32" && arch === "x64") {
15
+ return { target: "x86_64-pc-windows-msvc", archiveExt: "zip", exe: "ztnet.exe" };
16
+ }
17
+
18
+ if (platform === "linux" && arch === "x64") {
19
+ return { target: "x86_64-unknown-linux-gnu", archiveExt: "tar.gz", exe: "ztnet" };
20
+ }
21
+
22
+ if (platform === "darwin" && arch === "arm64") {
23
+ return { target: "aarch64-apple-darwin", archiveExt: "tar.gz", exe: "ztnet" };
24
+ }
25
+
26
+ if (platform === "darwin" && arch === "x64") {
27
+ return { target: "x86_64-apple-darwin", archiveExt: "tar.gz", exe: "ztnet" };
28
+ }
29
+
30
+ return null;
31
+ }
32
+
33
+ function readPackageJsonVersion(packageRoot) {
34
+ const pkgJsonPath = path.join(packageRoot, "package.json");
35
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
36
+ if (!pkg?.version) throw new Error("Failed to read version from package.json");
37
+ return String(pkg.version);
38
+ }
39
+
40
+ function httpGet(url) {
41
+ return new Promise((resolve, reject) => {
42
+ https
43
+ .get(
44
+ url,
45
+ {
46
+ headers: {
47
+ "User-Agent": "ztnet-cli-npm-installer",
48
+ Accept: "application/octet-stream",
49
+ },
50
+ },
51
+ (res) => {
52
+ const code = res.statusCode ?? 0;
53
+
54
+ if (code >= 300 && code < 400 && res.headers.location) {
55
+ res.resume();
56
+ resolve(httpGet(res.headers.location));
57
+ return;
58
+ }
59
+
60
+ if (code !== 200) {
61
+ const chunks = [];
62
+ res.on("data", (c) => chunks.push(c));
63
+ res.on("end", () => {
64
+ const body = Buffer.concat(chunks).toString("utf8").slice(0, 3000);
65
+ reject(new Error(`GET ${url} failed: HTTP ${code}\n${body}`));
66
+ });
67
+ return;
68
+ }
69
+
70
+ const chunks = [];
71
+ res.on("data", (c) => chunks.push(c));
72
+ res.on("end", () => resolve(Buffer.concat(chunks)));
73
+ res.on("error", reject);
74
+ },
75
+ )
76
+ .on("error", reject);
77
+ });
78
+ }
79
+
80
+ function downloadToFile(url, outPath) {
81
+ return new Promise((resolve, reject) => {
82
+ https
83
+ .get(
84
+ url,
85
+ {
86
+ headers: {
87
+ "User-Agent": "ztnet-cli-npm-installer",
88
+ Accept: "application/octet-stream",
89
+ },
90
+ },
91
+ (res) => {
92
+ const code = res.statusCode ?? 0;
93
+
94
+ if (code >= 300 && code < 400 && res.headers.location) {
95
+ res.resume();
96
+ resolve(downloadToFile(res.headers.location, outPath));
97
+ return;
98
+ }
99
+
100
+ if (code !== 200) {
101
+ const chunks = [];
102
+ res.on("data", (c) => chunks.push(c));
103
+ res.on("end", () => {
104
+ const body = Buffer.concat(chunks).toString("utf8").slice(0, 3000);
105
+ reject(new Error(`Download ${url} failed: HTTP ${code}\n${body}`));
106
+ });
107
+ return;
108
+ }
109
+
110
+ const file = fs.createWriteStream(outPath);
111
+ res.pipe(file);
112
+
113
+ file.on("finish", () => file.close(resolve));
114
+ file.on("error", (err) => {
115
+ try {
116
+ fs.unlinkSync(outPath);
117
+ } catch {
118
+ // ignore
119
+ }
120
+ reject(err);
121
+ });
122
+ },
123
+ )
124
+ .on("error", reject);
125
+ });
126
+ }
127
+
128
+ function sha256File(filePath) {
129
+ return new Promise((resolve, reject) => {
130
+ const hash = crypto.createHash("sha256");
131
+ const stream = fs.createReadStream(filePath);
132
+ stream.on("data", (chunk) => hash.update(chunk));
133
+ stream.on("error", reject);
134
+ stream.on("end", () => resolve(hash.digest("hex")));
135
+ });
136
+ }
137
+
138
+ function ensureDir(dirPath) {
139
+ fs.mkdirSync(dirPath, { recursive: true });
140
+ }
141
+
142
+ function safeCopyFile(src, dest) {
143
+ ensureDir(path.dirname(dest));
144
+ fs.copyFileSync(src, dest);
145
+ }
146
+
147
+ async function main() {
148
+ const packageRoot = path.join(__dirname, "..");
149
+ const resolved = resolvePlatformTarget();
150
+
151
+ if (!resolved) {
152
+ console.error(
153
+ `[ztnet-cli] Unsupported platform/arch: ${process.platform}/${process.arch}`,
154
+ );
155
+ process.exit(1);
156
+ }
157
+
158
+ const version = readPackageJsonVersion(packageRoot);
159
+ const repo = process.env.ZTNET_CLI_GITHUB_REPO || "JKamsker/ztnet-cli";
160
+ const tag = `v${version}`;
161
+
162
+ const asset = `ztnet-${version}-${resolved.target}.${resolved.archiveExt}`;
163
+ const assetUrl = `https://github.com/${repo}/releases/download/${tag}/${asset}`;
164
+ const shaUrl = `${assetUrl}.sha256`;
165
+ const localArtifactsDir = path.join(packageRoot, "artifacts");
166
+ const localArchivePath = path.join(localArtifactsDir, asset);
167
+ const localShaPath = `${localArchivePath}.sha256`;
168
+
169
+ const vendorDir = path.join(packageRoot, "vendor");
170
+ ensureDir(vendorDir);
171
+ const destBinary = path.join(vendorDir, resolved.exe);
172
+
173
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ztnet-cli-"));
174
+ const archivePath = path.join(tmpDir, asset);
175
+ const extractDir = path.join(tmpDir, "extract");
176
+ ensureDir(extractDir);
177
+
178
+ try {
179
+ const usingBundledAsset =
180
+ fs.existsSync(localArchivePath) && fs.existsSync(localShaPath);
181
+
182
+ const shaText = usingBundledAsset
183
+ ? fs.readFileSync(localShaPath, "utf8").trim()
184
+ : (await httpGet(shaUrl)).toString("utf8").trim();
185
+ const expectedHash = shaText.split(/\s+/)[0]?.trim();
186
+ if (!expectedHash || !/^[0-9a-fA-F]{64}$/.test(expectedHash)) {
187
+ const source = usingBundledAsset ? localShaPath : shaUrl;
188
+ throw new Error(`Invalid SHA256 file at ${source}`);
189
+ }
190
+
191
+ const actualArchivePath = usingBundledAsset ? localArchivePath : archivePath;
192
+
193
+ if (!usingBundledAsset) {
194
+ await downloadToFile(assetUrl, archivePath);
195
+ }
196
+
197
+ const actualHash = await sha256File(actualArchivePath);
198
+ if (actualHash.toLowerCase() !== expectedHash.toLowerCase()) {
199
+ throw new Error(
200
+ `SHA256 mismatch for ${asset}\nExpected: ${expectedHash}\nActual: ${actualHash}`,
201
+ );
202
+ }
203
+
204
+ if (resolved.archiveExt === "zip") {
205
+ await extractZip(actualArchivePath, { dir: extractDir });
206
+ } else if (resolved.archiveExt === "tar.gz") {
207
+ await tar.x({ file: actualArchivePath, cwd: extractDir });
208
+ } else {
209
+ throw new Error(`Unsupported archive type: ${resolved.archiveExt}`);
210
+ }
211
+
212
+ const extractedBinary = path.join(extractDir, resolved.exe);
213
+ if (!fs.existsSync(extractedBinary)) {
214
+ throw new Error(`Expected extracted binary not found: ${extractedBinary}`);
215
+ }
216
+
217
+ safeCopyFile(extractedBinary, destBinary);
218
+
219
+ if (process.platform !== "win32") {
220
+ fs.chmodSync(destBinary, 0o755);
221
+ }
222
+ } finally {
223
+ try {
224
+ fs.rmSync(tmpDir, { recursive: true, force: true });
225
+ } catch {
226
+ // ignore
227
+ }
228
+ }
229
+ }
230
+
231
+ main().catch((err) => {
232
+ console.error(`[ztnet-cli] Install failed: ${err?.message || String(err)}`);
233
+ process.exit(1);
234
+ });
235
+
@@ -0,0 +1,20 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const expected = process.env.VERSION;
5
+ if (!expected) {
6
+ console.error("[ztnet-cli] VERSION env var is required");
7
+ process.exit(1);
8
+ }
9
+
10
+ const pkgPath = path.join(__dirname, "..", "package.json");
11
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
12
+ const actual = String(pkg?.version ?? "");
13
+
14
+ if (actual !== String(expected)) {
15
+ console.error(
16
+ `[ztnet-cli] npm/package.json version ${actual} != ${String(expected)}`,
17
+ );
18
+ process.exit(1);
19
+ }
20
+