rainbo-cli 0.1.5 → 0.1.6

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 CHANGED
@@ -17,9 +17,9 @@ rainbo-cli --version
17
17
  ## Development
18
18
 
19
19
  ```bash
20
- Rscript src/rainbo-cli.R --version
20
+ Rscript bin/rainbo-cli.R --version
21
21
  npm run build:release
22
- node scripts/run.js help
22
+ node packaging/npm/run.js help
23
23
  ```
24
24
 
25
25
  ## Release
@@ -29,13 +29,21 @@ node scripts/run.js help
29
29
  3. Run:
30
30
 
31
31
  ```bash
32
- bash scripts/tag-release.sh
32
+ npm run release:tag
33
33
  ```
34
34
 
35
35
  Pushing the `vX.Y.Z` tag triggers GitHub Actions to create GitHub Release
36
36
  archives and publish the npm wrapper package.
37
37
 
38
- Required repository secret:
38
+ Publishing uses npm Trusted Publishing (OIDC) from GitHub Actions. No long-lived
39
+ `NPM_TOKEN` secret is required.
39
40
 
40
- - `NPM_TOKEN`: npm automation token with publish permission for `rainbo-cli`.
41
+ ## Project layout
41
42
 
43
+ ```text
44
+ bin/ Rscript entrypoint used by source and release archives
45
+ R/rainbo_cli/ CLI command, version, output, and update logic
46
+ packaging/npm/ npm postinstall downloader and bin launcher
47
+ tools/release/ release archive builder and tag helper
48
+ .github/workflows/ GitHub Release + npm Trusted Publishing workflow
49
+ ```
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- 20cd6f4f46d641bcdd574160533093743d2db7005ff3accb28df94973f30b5f1 rainbo-cli-0.1.5-darwin-amd64.tar.gz
2
- 75f63b5d3a47b63e3d74293b6998bd4d247e3d742a6b00923af58efe1b60d2ac rainbo-cli-0.1.5-darwin-arm64.tar.gz
3
- 8d03e90f6e6909b60378ed9157666d833a34ddb3b6ae5ee77094d6eb9e81c1a5 rainbo-cli-0.1.5-linux-amd64.tar.gz
4
- 744e9163cde429aae19b70f7d800c92826cc6c5d8e59e26855623684df85e372 rainbo-cli-0.1.5-linux-arm64.tar.gz
5
- e198b1df9f3684c49a5a65d41b6178d3270f1ca4a48111b05680feb42b3645a5 rainbo-cli-0.1.5-windows-amd64.zip
6
- e198b1df9f3684c49a5a65d41b6178d3270f1ca4a48111b05680feb42b3645a5 rainbo-cli-0.1.5-windows-arm64.zip
1
+ 1eefc9876d83642be4a2326447fc1cd1cdc2147b5c968efe3a7d954b5c990346 rainbo-cli-0.1.6-darwin-amd64.tar.gz
2
+ 50d9bdceae09a3b91d5faac0c3766e07f60b66cf9d25aae31c21ef9dfb8674db rainbo-cli-0.1.6-darwin-arm64.tar.gz
3
+ 2bd8eaaa8db300fe7366a84b551e68a5bb52567b34eb33831d237d0548e866b5 rainbo-cli-0.1.6-linux-amd64.tar.gz
4
+ fd6aa48c8af7bef44e1cde5118921f495666bd54c37e2f70de61c71e2cc42634 rainbo-cli-0.1.6-linux-arm64.tar.gz
5
+ 4141becb64dfbc4229d6969a08c2fb83566af8eb96c012717156cbf8c2f6d8e3 rainbo-cli-0.1.6-windows-amd64.zip
6
+ 4141becb64dfbc4229d6969a08c2fb83566af8eb96c012717156cbf8c2f6d8e3 rainbo-cli-0.1.6-windows-arm64.zip
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "rainbo-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Rainbo command line tools for data migration workflows",
5
5
  "bin": {
6
- "rainbo-cli": "scripts/run.js"
6
+ "rainbo-cli": "packaging/npm/run.js"
7
7
  },
8
8
  "scripts": {
9
- "build:release": "bash scripts/build-release.sh",
10
- "postinstall": "node scripts/install.js",
11
- "test": "node scripts/run.js --version && node scripts/run.js help"
9
+ "build:release": "bash tools/release/build-release.sh",
10
+ "postinstall": "node packaging/npm/install.js",
11
+ "release:tag": "bash tools/release/tag-release.sh",
12
+ "test": "node packaging/npm/run.js --version && node packaging/npm/run.js help && node packaging/npm/run.js update --check"
12
13
  },
13
14
  "os": [
14
15
  "darwin",
@@ -28,8 +29,8 @@
28
29
  },
29
30
  "license": "MIT",
30
31
  "files": [
31
- "scripts/install.js",
32
- "scripts/run.js",
32
+ "packaging/npm/install.js",
33
+ "packaging/npm/run.js",
33
34
  "checksums.txt",
34
35
  "README.md",
35
36
  "LICENSE"
@@ -0,0 +1,167 @@
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 Rscript is installed and the v${VERSION} GitHub Release exists.`);
162
+ process.exit(1);
163
+ }
164
+ }
165
+
166
+ module.exports = { getExpectedChecksum, verifyChecksum, resolveMirrorUrls, assertAllowedHost };
167
+
@@ -5,40 +5,46 @@ const fs = require("fs");
5
5
  const path = require("path");
6
6
 
7
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");
8
+ const repoRoot = path.join(__dirname, "..", "..");
9
+ const vendorRoot = path.join(repoRoot, "vendor", "rainbo-cli");
10
+ const launcher = path.join(vendorRoot, isWindows ? "rainbo-cli.cmd" : "rainbo-cli");
11
+ const sourceEntry = path.join(repoRoot, "bin", "rainbo-cli.R");
16
12
 
17
13
  function installIfMissing() {
18
- if (fs.existsSync(launcher)) return;
19
- if (fs.existsSync(sourceEntry)) return;
20
-
14
+ if (fs.existsSync(launcher) || fs.existsSync(sourceEntry)) return;
21
15
  const result = spawnSync(process.execPath, [path.join(__dirname, "install.js")], {
22
16
  stdio: "inherit",
23
17
  env: { ...process.env, RAINBO_CLI_RUN: "true" },
24
18
  });
19
+ if (result.status !== 0) process.exit(result.status || 1);
20
+ }
25
21
 
26
- if (result.status !== 0) {
27
- process.exit(result.status || 1);
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.
28
34
  }
29
35
  }
30
36
 
37
+ recoverWindowsOldLauncher();
31
38
  installIfMissing();
32
39
 
33
40
  const useSource = fs.existsSync(sourceEntry);
34
41
  const command = useSource ? "Rscript" : launcher;
35
42
  const args = useSource ? [sourceEntry, ...process.argv.slice(2)] : process.argv.slice(2);
36
-
37
43
  const result = spawnSync(command, args, { stdio: "inherit", shell: isWindows });
38
44
 
39
45
  if (result.error) {
40
46
  console.error(result.error.message);
41
47
  process.exit(1);
42
48
  }
43
-
44
49
  process.exit(result.status || 0);
50
+
@@ -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
-