rainbo-cli 0.1.5 → 0.1.7
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 +69 -7
- package/checksums.txt +6 -6
- package/package.json +10 -7
- package/packaging/npm/install.js +166 -0
- package/packaging/npm/run.js +51 -0
- package/scripts/install.js +0 -143
- package/scripts/run.js +0 -44
package/README.md
CHANGED
|
@@ -5,23 +5,64 @@ Rainbo command line tools for data migration workflows.
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- Node.js 16+
|
|
8
|
-
-
|
|
8
|
+
- Rust toolchain for source development. End users installing from npm get a prebuilt binary.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
npm install -g rainbo-cli
|
|
14
|
-
rainbo-cli
|
|
14
|
+
rainbo-cli version
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Development
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
|
|
20
|
+
cargo run --manifest-path rust/Cargo.toml -- --version
|
|
21
21
|
npm run build:release
|
|
22
|
-
node
|
|
22
|
+
node packaging/npm/run.js help
|
|
23
|
+
npm run check:skills
|
|
23
24
|
```
|
|
24
25
|
|
|
26
|
+
## Output
|
|
27
|
+
|
|
28
|
+
Command output is structured for humans, scripts, and AI agents:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
rainbo-cli version # JSON envelope on stdout
|
|
32
|
+
rainbo-cli version --format pretty # Human-readable output
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Successful JSON output uses:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{"ok":true,"data":{...}}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Errors are written to stderr:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{"ok":false,"error":{"type":"validation","subtype":"invalid_argument","message":"..."}}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Update
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
rainbo-cli update --check
|
|
51
|
+
rainbo-cli update
|
|
52
|
+
rainbo-cli update --force
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`update --check` only reports status. `update` detects npm installations,
|
|
56
|
+
runs `npm install -g rainbo-cli@<latest>` when auto-update is available,
|
|
57
|
+
verifies the new binary, and syncs repository skills with:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx skills add work2a/rainbo-cli -y -g
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If automatic update is unavailable, the JSON action is `manual_required` and
|
|
64
|
+
the response includes release and changelog URLs.
|
|
65
|
+
|
|
25
66
|
## Release
|
|
26
67
|
|
|
27
68
|
1. Update `package.json` version.
|
|
@@ -29,13 +70,34 @@ node scripts/run.js help
|
|
|
29
70
|
3. Run:
|
|
30
71
|
|
|
31
72
|
```bash
|
|
32
|
-
|
|
73
|
+
npm run release:tag
|
|
33
74
|
```
|
|
34
75
|
|
|
35
76
|
Pushing the `vX.Y.Z` tag triggers GitHub Actions to create GitHub Release
|
|
36
77
|
archives and publish the npm wrapper package.
|
|
37
78
|
|
|
38
|
-
|
|
79
|
+
Publishing uses npm Trusted Publishing (OIDC) from GitHub Actions. No long-lived
|
|
80
|
+
`NPM_TOKEN` secret is required.
|
|
81
|
+
|
|
82
|
+
## Skills
|
|
39
83
|
|
|
40
|
-
|
|
84
|
+
Repository skills live in `skills/`. They can be installed from the repository:
|
|
41
85
|
|
|
86
|
+
```bash
|
|
87
|
+
npx skills add work2a/rainbo-cli#main -y -g
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Pull requests publish a preview npm package with `pkg.pr.new` and comment with
|
|
91
|
+
both the CLI update command and the matching skill install command.
|
|
92
|
+
|
|
93
|
+
## Project layout
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
rust/ Rust CLI implementation
|
|
97
|
+
packaging/npm/ npm postinstall downloader and bin launcher
|
|
98
|
+
tools/release/ release archive builder and tag helper
|
|
99
|
+
skills/ Rainbo skills installable with `npx skills add`
|
|
100
|
+
skill-template/ Template for new Rainbo skills
|
|
101
|
+
scripts/ PR preview packaging and skill format checks
|
|
102
|
+
.github/workflows/ GitHub Release + npm Trusted Publishing workflow
|
|
103
|
+
```
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
3ed156f93253d1aa1f33c8d4551d397ca623fed93a4bbc59da0030779430e15b rainbo-cli-0.1.7-darwin-amd64.tar.gz
|
|
2
|
+
37e7cc7c758996b05208c8464fee05b8160a1e4bfc845f4035cbc939855903b0 rainbo-cli-0.1.7-darwin-arm64.tar.gz
|
|
3
|
+
4b21440ba1919bbfaf35759e0596e9072dab937bf70f79f72122e681f897bd30 rainbo-cli-0.1.7-linux-amd64.tar.gz
|
|
4
|
+
8842f49579994fe08cf273fd1f0a3ed436886087337c32810c683820c94c5f92 rainbo-cli-0.1.7-linux-arm64.tar.gz
|
|
5
|
+
44779d87de1f1fb5018f69a4ed021162531f4cf99e3de491d2da8747cf370652 rainbo-cli-0.1.7-windows-amd64.zip
|
|
6
|
+
691d327c1cbbd4ef1bead22c5561a762890f860d73fd1899c6cda6f7ce13c3d0 rainbo-cli-0.1.7-windows-arm64.zip
|
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rainbo-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Rainbo command line tools for data migration workflows",
|
|
5
5
|
"bin": {
|
|
6
|
-
"rainbo-cli": "
|
|
6
|
+
"rainbo-cli": "packaging/npm/run.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build:release": "bash
|
|
10
|
-
"
|
|
11
|
-
"
|
|
9
|
+
"build:release": "bash tools/release/build-release.sh",
|
|
10
|
+
"build:pkg-pr-new": "bash scripts/build-pkg-pr-new.sh",
|
|
11
|
+
"check:skills": "node scripts/skill-format-check/index.js",
|
|
12
|
+
"postinstall": "node packaging/npm/install.js",
|
|
13
|
+
"release:tag": "bash tools/release/tag-release.sh",
|
|
14
|
+
"test": "cargo test --manifest-path rust/Cargo.toml && node packaging/npm/run.js version && node packaging/npm/run.js help && node packaging/npm/run.js update --check"
|
|
12
15
|
},
|
|
13
16
|
"os": [
|
|
14
17
|
"darwin",
|
|
@@ -28,8 +31,8 @@
|
|
|
28
31
|
},
|
|
29
32
|
"license": "MIT",
|
|
30
33
|
"files": [
|
|
31
|
-
"
|
|
32
|
-
"
|
|
34
|
+
"packaging/npm/install.js",
|
|
35
|
+
"packaging/npm/run.js",
|
|
33
36
|
"checksums.txt",
|
|
34
37
|
"README.md",
|
|
35
38
|
"LICENSE"
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { execFileSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.join(__dirname, "..", "..");
|
|
10
|
+
const VERSION = require(path.join(repoRoot, "package.json")).version.replace(/-.*$/, "");
|
|
11
|
+
const REPO = "work2a/rainbo-cli";
|
|
12
|
+
const NAME = "rainbo-cli";
|
|
13
|
+
const DEFAULT_MIRROR_HOST = "https://registry.npmmirror.com";
|
|
14
|
+
const ALLOWED_HOSTS = new Set(["github.com", "objects.githubusercontent.com", "registry.npmmirror.com"]);
|
|
15
|
+
|
|
16
|
+
const PLATFORM_MAP = { darwin: "darwin", linux: "linux", win32: "windows" };
|
|
17
|
+
const ARCH_MAP = { x64: "amd64", arm64: "arm64" };
|
|
18
|
+
|
|
19
|
+
const platform = PLATFORM_MAP[process.platform];
|
|
20
|
+
const arch = ARCH_MAP[process.arch];
|
|
21
|
+
const isWindows = process.platform === "win32";
|
|
22
|
+
const ext = isWindows ? ".zip" : ".tar.gz";
|
|
23
|
+
const archiveName = `${NAME}-${VERSION}-${platform}-${arch}${ext}`;
|
|
24
|
+
const githubUrl = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
|
|
25
|
+
const installDir = path.join(repoRoot, "vendor", NAME);
|
|
26
|
+
|
|
27
|
+
function joinUrl(base, suffix) {
|
|
28
|
+
return base.replace(/\/+$/, "") + suffix;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isDefaultNpmjsRegistry(url) {
|
|
32
|
+
try {
|
|
33
|
+
return new URL(url).hostname === "registry.npmjs.org";
|
|
34
|
+
} catch (_) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isValidDownloadBase(raw) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = new URL(raw);
|
|
42
|
+
return parsed.protocol === "https:" && !!parsed.hostname;
|
|
43
|
+
} catch (_) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveMirrorUrls(env, archive, version) {
|
|
49
|
+
const binaryPath = `/-/binary/${NAME}/v${version}/${archive}`;
|
|
50
|
+
const urls = [];
|
|
51
|
+
const registry = (env.npm_config_registry || "").trim();
|
|
52
|
+
if (registry && !isDefaultNpmjsRegistry(registry) && isValidDownloadBase(registry)) {
|
|
53
|
+
const base = new URL(registry);
|
|
54
|
+
urls.push(joinUrl(base.origin + base.pathname, binaryPath));
|
|
55
|
+
}
|
|
56
|
+
const defaultUrl = joinUrl(DEFAULT_MIRROR_HOST, binaryPath);
|
|
57
|
+
if (!urls.includes(defaultUrl)) urls.push(defaultUrl);
|
|
58
|
+
return urls;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertAllowedHost(url) {
|
|
62
|
+
const host = new URL(url).hostname;
|
|
63
|
+
if (!ALLOWED_HOSTS.has(host)) throw new Error(`Download host not allowed: ${host}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function download(url, destPath) {
|
|
67
|
+
assertAllowedHost(url);
|
|
68
|
+
execFileSync("curl", [
|
|
69
|
+
"--fail", "--location", "--silent", "--show-error",
|
|
70
|
+
"--connect-timeout", "10", "--max-time", "120", "--max-redirs", "3",
|
|
71
|
+
"--output", destPath, url,
|
|
72
|
+
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getExpectedChecksum(archive, checksumsDir) {
|
|
76
|
+
const checksumsPath = path.join(checksumsDir || repoRoot, "checksums.txt");
|
|
77
|
+
if (!fs.existsSync(checksumsPath)) {
|
|
78
|
+
console.error("[WARN] checksums.txt not found, skipping checksum verification");
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const content = fs.readFileSync(checksumsPath, "utf8");
|
|
82
|
+
for (const line of content.split("\n")) {
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
if (!trimmed) continue;
|
|
85
|
+
const idx = trimmed.indexOf(" ");
|
|
86
|
+
if (idx === -1) continue;
|
|
87
|
+
if (trimmed.slice(idx + 2) === archive) return trimmed.slice(0, idx);
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Checksum entry not found for ${archive}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function verifyChecksum(archivePath, expectedHash) {
|
|
93
|
+
if (expectedHash === null) return;
|
|
94
|
+
const hash = crypto.createHash("sha256");
|
|
95
|
+
const fd = fs.openSync(archivePath, "r");
|
|
96
|
+
try {
|
|
97
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
98
|
+
let bytesRead;
|
|
99
|
+
while ((bytesRead = fs.readSync(fd, buf, 0, buf.length, null)) > 0) {
|
|
100
|
+
hash.update(buf.subarray(0, bytesRead));
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
fs.closeSync(fd);
|
|
104
|
+
}
|
|
105
|
+
const actual = hash.digest("hex");
|
|
106
|
+
if (actual.toLowerCase() !== expectedHash.toLowerCase()) {
|
|
107
|
+
throw new Error(`Checksum mismatch for ${path.basename(archivePath)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extractArchive(archivePath, destDir) {
|
|
112
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
113
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
114
|
+
if (isWindows) {
|
|
115
|
+
const psCommand = "$ErrorActionPreference='Stop';" +
|
|
116
|
+
"Expand-Archive -LiteralPath $env:RAINBO_CLI_ARCHIVE -DestinationPath $env:RAINBO_CLI_DEST -Force";
|
|
117
|
+
execFileSync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", psCommand], {
|
|
118
|
+
stdio: "inherit",
|
|
119
|
+
env: { ...process.env, RAINBO_CLI_ARCHIVE: archivePath, RAINBO_CLI_DEST: destDir },
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
execFileSync("tar", ["-xzf", archivePath, "-C", destDir, "--strip-components", "1"], { stdio: "inherit" });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function install() {
|
|
127
|
+
if (!platform || !arch) throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
|
|
128
|
+
const mirrors = resolveMirrorUrls(process.env, archiveName, VERSION);
|
|
129
|
+
for (const url of mirrors) ALLOWED_HOSTS.add(new URL(url).hostname);
|
|
130
|
+
const urls = [githubUrl, ...mirrors];
|
|
131
|
+
|
|
132
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `${NAME}-`));
|
|
133
|
+
const archivePath = path.join(tmpDir, archiveName);
|
|
134
|
+
try {
|
|
135
|
+
let lastErr;
|
|
136
|
+
for (const url of urls) {
|
|
137
|
+
try {
|
|
138
|
+
download(url, archivePath);
|
|
139
|
+
lastErr = null;
|
|
140
|
+
break;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
lastErr = err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (lastErr) throw lastErr;
|
|
146
|
+
verifyChecksum(archivePath, getExpectedChecksum(archiveName));
|
|
147
|
+
extractArchive(archivePath, installDir);
|
|
148
|
+
console.log(`${NAME} v${VERSION} installed successfully`);
|
|
149
|
+
} finally {
|
|
150
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (require.main === module) {
|
|
155
|
+
const isNpxPostinstall = process.env.npm_command === "exec" && !process.env.RAINBO_CLI_RUN;
|
|
156
|
+
if (isNpxPostinstall) process.exit(0);
|
|
157
|
+
try {
|
|
158
|
+
install();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(`Failed to install ${NAME}: ${err.message}`);
|
|
161
|
+
console.error(`Make sure the v${VERSION} GitHub Release exists for your platform.`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { getExpectedChecksum, verifyChecksum, resolveMirrorUrls, assertAllowedHost };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const isWindows = process.platform === "win32";
|
|
8
|
+
const repoRoot = path.join(__dirname, "..", "..");
|
|
9
|
+
const vendorRoot = path.join(repoRoot, "vendor", "rainbo-cli");
|
|
10
|
+
const launcher = path.join(vendorRoot, isWindows ? "rainbo-cli.exe" : "rainbo-cli");
|
|
11
|
+
const sourceManifest = path.join(repoRoot, "rust", "Cargo.toml");
|
|
12
|
+
|
|
13
|
+
function installIfMissing() {
|
|
14
|
+
if (fs.existsSync(launcher) || fs.existsSync(sourceManifest)) return;
|
|
15
|
+
const result = spawnSync(process.execPath, [path.join(__dirname, "install.js")], {
|
|
16
|
+
stdio: "inherit",
|
|
17
|
+
env: { ...process.env, RAINBO_CLI_RUN: "true" },
|
|
18
|
+
});
|
|
19
|
+
if (result.status !== 0) process.exit(result.status || 1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function recoverWindowsOldLauncher() {
|
|
23
|
+
if (!isWindows) return;
|
|
24
|
+
const oldLauncher = launcher + ".old";
|
|
25
|
+
if (!fs.existsSync(oldLauncher)) return;
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(launcher)) {
|
|
28
|
+
fs.renameSync(oldLauncher, launcher);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
fs.rmSync(oldLauncher, { force: true });
|
|
32
|
+
} catch (_) {
|
|
33
|
+
// Best-effort cleanup only.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
recoverWindowsOldLauncher();
|
|
38
|
+
installIfMissing();
|
|
39
|
+
|
|
40
|
+
const useSource = fs.existsSync(sourceManifest);
|
|
41
|
+
const command = useSource ? "cargo" : launcher;
|
|
42
|
+
const args = useSource
|
|
43
|
+
? ["run", "--quiet", "--manifest-path", sourceManifest, "--", ...process.argv.slice(2)]
|
|
44
|
+
: process.argv.slice(2);
|
|
45
|
+
const result = spawnSync(command, args, { stdio: "inherit", shell: isWindows });
|
|
46
|
+
|
|
47
|
+
if (result.error) {
|
|
48
|
+
console.error(result.error.message);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
process.exit(result.status || 0);
|
package/scripts/install.js
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const crypto = require("crypto");
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
const os = require("os");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
const { execFileSync } = require("child_process");
|
|
8
|
-
|
|
9
|
-
const VERSION = require("../package.json").version.replace(/-.*$/, "");
|
|
10
|
-
const REPO = "work2a/rainbo-cli";
|
|
11
|
-
const NAME = "rainbo-cli";
|
|
12
|
-
|
|
13
|
-
const PLATFORM_MAP = {
|
|
14
|
-
darwin: "darwin",
|
|
15
|
-
linux: "linux",
|
|
16
|
-
win32: "windows",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const ARCH_MAP = {
|
|
20
|
-
x64: "amd64",
|
|
21
|
-
arm64: "arm64",
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const platform = PLATFORM_MAP[process.platform];
|
|
25
|
-
const arch = ARCH_MAP[process.arch];
|
|
26
|
-
const isWindows = process.platform === "win32";
|
|
27
|
-
const ext = isWindows ? ".zip" : ".tar.gz";
|
|
28
|
-
const archiveName = `${NAME}-${VERSION}-${platform}-${arch}${ext}`;
|
|
29
|
-
const githubUrl = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
|
|
30
|
-
const vendorDir = path.join(__dirname, "..", "vendor");
|
|
31
|
-
const installDir = path.join(vendorDir, NAME);
|
|
32
|
-
|
|
33
|
-
function download(url, destPath) {
|
|
34
|
-
execFileSync("curl", [
|
|
35
|
-
"--fail",
|
|
36
|
-
"--location",
|
|
37
|
-
"--silent",
|
|
38
|
-
"--show-error",
|
|
39
|
-
"--connect-timeout",
|
|
40
|
-
"10",
|
|
41
|
-
"--max-time",
|
|
42
|
-
"120",
|
|
43
|
-
"--max-redirs",
|
|
44
|
-
"3",
|
|
45
|
-
"--output",
|
|
46
|
-
destPath,
|
|
47
|
-
url,
|
|
48
|
-
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function getExpectedChecksum(archive) {
|
|
52
|
-
const checksumsPath = path.join(__dirname, "..", "checksums.txt");
|
|
53
|
-
if (!fs.existsSync(checksumsPath)) {
|
|
54
|
-
console.error("[WARN] checksums.txt not found, skipping checksum verification");
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const content = fs.readFileSync(checksumsPath, "utf8");
|
|
59
|
-
for (const line of content.split("\n")) {
|
|
60
|
-
const trimmed = line.trim();
|
|
61
|
-
if (!trimmed) continue;
|
|
62
|
-
const idx = trimmed.indexOf(" ");
|
|
63
|
-
if (idx === -1) continue;
|
|
64
|
-
if (trimmed.slice(idx + 2) === archive) return trimmed.slice(0, idx);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
throw new Error(`Checksum entry not found for ${archive}`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function verifyChecksum(archivePath, expectedHash) {
|
|
71
|
-
if (expectedHash === null) return;
|
|
72
|
-
|
|
73
|
-
const actual = crypto.createHash("sha256")
|
|
74
|
-
.update(fs.readFileSync(archivePath))
|
|
75
|
-
.digest("hex");
|
|
76
|
-
|
|
77
|
-
if (actual.toLowerCase() !== expectedHash.toLowerCase()) {
|
|
78
|
-
throw new Error(`Checksum mismatch for ${path.basename(archivePath)}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function extractArchive(archivePath, destDir) {
|
|
83
|
-
fs.rmSync(destDir, { recursive: true, force: true });
|
|
84
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
85
|
-
|
|
86
|
-
if (isWindows) {
|
|
87
|
-
const psCommand =
|
|
88
|
-
"$ErrorActionPreference='Stop';" +
|
|
89
|
-
"Expand-Archive -LiteralPath $env:RAINBO_CLI_ARCHIVE -DestinationPath $env:RAINBO_CLI_DEST -Force";
|
|
90
|
-
execFileSync("powershell.exe", [
|
|
91
|
-
"-NoProfile",
|
|
92
|
-
"-ExecutionPolicy",
|
|
93
|
-
"Bypass",
|
|
94
|
-
"-Command",
|
|
95
|
-
psCommand,
|
|
96
|
-
], {
|
|
97
|
-
stdio: "inherit",
|
|
98
|
-
env: {
|
|
99
|
-
...process.env,
|
|
100
|
-
RAINBO_CLI_ARCHIVE: archivePath,
|
|
101
|
-
RAINBO_CLI_DEST: destDir,
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
} else {
|
|
105
|
-
execFileSync("tar", ["-xzf", archivePath, "-C", destDir, "--strip-components", "1"], {
|
|
106
|
-
stdio: "inherit",
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function install() {
|
|
112
|
-
if (!platform || !arch) {
|
|
113
|
-
throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `${NAME}-`));
|
|
117
|
-
const archivePath = path.join(tmpDir, archiveName);
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
download(githubUrl, archivePath);
|
|
121
|
-
verifyChecksum(archivePath, getExpectedChecksum(archiveName));
|
|
122
|
-
extractArchive(archivePath, installDir);
|
|
123
|
-
console.log(`${NAME} v${VERSION} installed successfully`);
|
|
124
|
-
} finally {
|
|
125
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (require.main === module) {
|
|
130
|
-
const isNpxPostinstall = process.env.npm_command === "exec" && !process.env.RAINBO_CLI_RUN;
|
|
131
|
-
if (isNpxPostinstall) process.exit(0);
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
install();
|
|
135
|
-
} catch (err) {
|
|
136
|
-
console.error(`Failed to install ${NAME}: ${err.message}`);
|
|
137
|
-
console.error(`Make sure the v${VERSION} GitHub Release exists and Rscript is installed.`);
|
|
138
|
-
process.exit(1);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
module.exports = { getExpectedChecksum, verifyChecksum };
|
|
143
|
-
|
package/scripts/run.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const { spawnSync } = require("child_process");
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
const path = require("path");
|
|
6
|
-
|
|
7
|
-
const isWindows = process.platform === "win32";
|
|
8
|
-
const launcher = path.join(
|
|
9
|
-
__dirname,
|
|
10
|
-
"..",
|
|
11
|
-
"vendor",
|
|
12
|
-
"rainbo-cli",
|
|
13
|
-
isWindows ? "rainbo-cli.cmd" : "rainbo-cli"
|
|
14
|
-
);
|
|
15
|
-
const sourceEntry = path.join(__dirname, "..", "src", "rainbo-cli.R");
|
|
16
|
-
|
|
17
|
-
function installIfMissing() {
|
|
18
|
-
if (fs.existsSync(launcher)) return;
|
|
19
|
-
if (fs.existsSync(sourceEntry)) return;
|
|
20
|
-
|
|
21
|
-
const result = spawnSync(process.execPath, [path.join(__dirname, "install.js")], {
|
|
22
|
-
stdio: "inherit",
|
|
23
|
-
env: { ...process.env, RAINBO_CLI_RUN: "true" },
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
if (result.status !== 0) {
|
|
27
|
-
process.exit(result.status || 1);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
installIfMissing();
|
|
32
|
-
|
|
33
|
-
const useSource = fs.existsSync(sourceEntry);
|
|
34
|
-
const command = useSource ? "Rscript" : launcher;
|
|
35
|
-
const args = useSource ? [sourceEntry, ...process.argv.slice(2)] : process.argv.slice(2);
|
|
36
|
-
|
|
37
|
-
const result = spawnSync(command, args, { stdio: "inherit", shell: isWindows });
|
|
38
|
-
|
|
39
|
-
if (result.error) {
|
|
40
|
-
console.error(result.error.message);
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
process.exit(result.status || 0);
|