@rfabric/cli 0.0.1

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/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@rfabric/cli",
3
+ "version": "0.0.1",
4
+ "description": "rFabric CLI — terminal-native interface to the rFabric robotics lifecycle platform",
5
+ "homepage": "https://rfabric.io",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/rfabric/cli.git"
9
+ },
10
+ "license": "Apache-2.0",
11
+ "bin": {
12
+ "rfabric": "bin/rfabric.js"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "scripts/"
17
+ ],
18
+ "scripts": {
19
+ "postinstall": "node scripts/postinstall.js"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "os": [
25
+ "darwin",
26
+ "linux",
27
+ "win32"
28
+ ],
29
+ "cpu": [
30
+ "x64",
31
+ "arm64"
32
+ ]
33
+ }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const https = require("node:https");
7
+ const crypto = require("node:crypto");
8
+ const { spawnSync } = require("node:child_process");
9
+
10
+ const REPO = "rfabric/cli";
11
+ const packageJson = require("../package.json");
12
+ const version = packageJson.version;
13
+
14
+ const platformMap = { darwin: "darwin", linux: "linux", win32: "windows" };
15
+ const archMap = { x64: "amd64", arm64: "arm64" };
16
+
17
+ const platform = platformMap[process.platform];
18
+ const arch = archMap[process.arch];
19
+
20
+ if (!platform || !arch) {
21
+ fail(`unsupported platform: ${process.platform}/${process.arch}`);
22
+ }
23
+
24
+ const isWindows = platform === "windows";
25
+ const archiveExt = isWindows ? "zip" : "tar.gz";
26
+ const binaryName = isWindows ? "rfabric.exe" : "rfabric";
27
+ const archiveName = `rfabric_${version}_${platform}_${arch}.${archiveExt}`;
28
+ const baseUrl = `https://github.com/${REPO}/releases/download/v${version}`;
29
+
30
+ const vendorDir = path.join(__dirname, "..", "vendor");
31
+ const binaryPath = path.join(vendorDir, binaryName);
32
+
33
+ main().catch((error) => fail(error.message || String(error)));
34
+
35
+ async function main() {
36
+ if (fs.existsSync(binaryPath)) {
37
+ return;
38
+ }
39
+
40
+ fs.mkdirSync(vendorDir, { recursive: true });
41
+
42
+ log(`downloading ${archiveName}`);
43
+ const archiveBuffer = await downloadToBuffer(`${baseUrl}/${archiveName}`);
44
+
45
+ log("verifying checksum");
46
+ const checksumsText = (await downloadToBuffer(`${baseUrl}/checksums.txt`)).toString("utf8");
47
+ const expectedChecksum = parseChecksum(checksumsText, archiveName);
48
+ const actualChecksum = crypto.createHash("sha256").update(archiveBuffer).digest("hex");
49
+ if (expectedChecksum !== actualChecksum) {
50
+ fail(`checksum mismatch (expected ${expectedChecksum}, got ${actualChecksum})`);
51
+ }
52
+
53
+ log("extracting");
54
+ if (isWindows) {
55
+ extractZip(archiveBuffer, vendorDir);
56
+ } else {
57
+ await extractTarGz(archiveBuffer, vendorDir);
58
+ }
59
+
60
+ if (!fs.existsSync(binaryPath)) {
61
+ fail(`binary ${binaryName} not found in archive`);
62
+ }
63
+
64
+ if (!isWindows) {
65
+ fs.chmodSync(binaryPath, 0o755);
66
+ }
67
+
68
+ log(`installed to ${binaryPath}`);
69
+ }
70
+
71
+ function downloadToBuffer(url, redirects = 0) {
72
+ return new Promise((resolve, reject) => {
73
+ if (redirects > 5) {
74
+ return reject(new Error(`too many redirects fetching ${url}`));
75
+ }
76
+ https
77
+ .get(url, { headers: { "User-Agent": "rfabric-npm-postinstall" } }, (response) => {
78
+ if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) {
79
+ return resolve(downloadToBuffer(response.headers.location, redirects + 1));
80
+ }
81
+ if (response.statusCode !== 200) {
82
+ return reject(new Error(`HTTP ${response.statusCode} fetching ${url}`));
83
+ }
84
+ const chunks = [];
85
+ response.on("data", (chunk) => chunks.push(chunk));
86
+ response.on("end", () => resolve(Buffer.concat(chunks)));
87
+ response.on("error", reject);
88
+ })
89
+ .on("error", reject);
90
+ });
91
+ }
92
+
93
+ function parseChecksum(checksumsText, fileName) {
94
+ for (const line of checksumsText.split(/\r?\n/)) {
95
+ const match = line.match(/^([a-fA-F0-9]+)\s+(\S+)$/);
96
+ if (match && match[2] === fileName) {
97
+ return match[1];
98
+ }
99
+ }
100
+ fail(`no checksum entry for ${fileName}`);
101
+ }
102
+
103
+ async function extractTarGz(buffer, targetDir) {
104
+ const result = spawnSync("tar", ["-xzf", "-", "-C", targetDir], { input: buffer });
105
+ if (result.status !== 0) {
106
+ fail(`tar extraction failed: ${result.stderr?.toString() || "unknown error"}`);
107
+ }
108
+ }
109
+
110
+ function extractZip(buffer, targetDir) {
111
+ const tmp = path.join(targetDir, `__archive__.zip`);
112
+ fs.writeFileSync(tmp, buffer);
113
+ try {
114
+ const result = spawnSync(
115
+ "powershell",
116
+ ["-NoProfile", "-Command", `Expand-Archive -Force -Path "${tmp}" -DestinationPath "${targetDir}"`],
117
+ { stdio: "inherit" }
118
+ );
119
+ if (result.status !== 0) {
120
+ fail("zip extraction failed");
121
+ }
122
+ } finally {
123
+ fs.unlinkSync(tmp);
124
+ }
125
+ }
126
+
127
+ function log(message) {
128
+ process.stdout.write(`[rfabric] ${message}\n`);
129
+ }
130
+
131
+ function fail(message) {
132
+ process.stderr.write(`[rfabric] error: ${message}\n`);
133
+ process.exit(1);
134
+ }