@clovapi/cli 0.1.0

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 ADDED
@@ -0,0 +1,19 @@
1
+ # @clovapi/cli
2
+
3
+ Install `clovapi` as a global npm command:
4
+
5
+ ```bash
6
+ npm i -g @clovapi/cli
7
+ clovapi version
8
+ ```
9
+
10
+ The package downloads platform binaries and verifies SHA256 checksums.
11
+ By default it tries:
12
+
13
+ 1. `https://downloads.clovapi.com/clovapi/vX.Y.Z` (R2 public mirror)
14
+ 2. GitHub Releases fallback
15
+
16
+ ## Environment override
17
+
18
+ - `CLOVAPI_CLI_BASE_URL`: override download base URL for mirrors or local testing.
19
+ - `CLOVAPI_R2_BASE_URL`: override the default R2 base URL (versioned path).
package/bin/clovapi.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawnSync } = require("node:child_process");
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+
7
+ const exeName = process.platform === "win32" ? "clovapi.exe" : "clovapi";
8
+ const binPath = path.join(__dirname, "..", "vendor", exeName);
9
+
10
+ if (!fs.existsSync(binPath)) {
11
+ console.error("clovapi binary is not installed.");
12
+ console.error("Reinstall with: npm i -g @clovapi/cli");
13
+ process.exit(1);
14
+ }
15
+
16
+ const result = spawnSync(binPath, process.argv.slice(2), {
17
+ stdio: "inherit",
18
+ });
19
+
20
+ if (result.error) {
21
+ console.error(result.error.message);
22
+ process.exit(1);
23
+ }
24
+
25
+ process.exit(result.status ?? 0);
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@clovapi/cli",
3
+ "version": "0.1.0",
4
+ "description": "Install clovapi CLI from GitHub Releases",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/joohw/clovapi.git",
8
+ "directory": "switcher/npm"
9
+ },
10
+ "license": "MIT",
11
+ "bin": {
12
+ "clovapi": "bin/clovapi.js"
13
+ },
14
+ "scripts": {
15
+ "postinstall": "node scripts/install.js"
16
+ },
17
+ "files": [
18
+ "bin",
19
+ "scripts",
20
+ "vendor",
21
+ "README.md"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "dependencies": {
27
+ "adm-zip": "^0.5.16",
28
+ "tar": "^7.4.3"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ }
33
+ }
@@ -0,0 +1,173 @@
1
+ const crypto = require("node:crypto");
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+
6
+ const AdmZip = require("adm-zip");
7
+ const tar = require("tar");
8
+
9
+ const pkg = require("../package.json");
10
+
11
+ const PLATFORM_MAP = {
12
+ darwin: "darwin",
13
+ linux: "linux",
14
+ win32: "windows",
15
+ };
16
+
17
+ const ARCH_MAP = {
18
+ x64: "amd64",
19
+ arm64: "arm64",
20
+ };
21
+
22
+ function fail(message) {
23
+ console.error(`[clovapi install] ${message}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ function getReleaseCoordinates() {
28
+ const osName = PLATFORM_MAP[process.platform];
29
+ if (!osName) {
30
+ fail(`unsupported platform: ${process.platform}`);
31
+ }
32
+
33
+ const archName = ARCH_MAP[process.arch];
34
+ if (!archName) {
35
+ fail(`unsupported arch: ${process.arch}`);
36
+ }
37
+
38
+ const versionTag = `v${pkg.version}`;
39
+ const ext = osName === "windows" ? "zip" : "tar.gz";
40
+ const archiveName = `clovapi_${versionTag}_${osName}_${archName}.${ext}`;
41
+
42
+ return {
43
+ osName,
44
+ archName,
45
+ versionTag,
46
+ archiveName,
47
+ };
48
+ }
49
+
50
+ async function download(url) {
51
+ const response = await fetch(url);
52
+ if (!response.ok) {
53
+ throw new Error(`download failed ${response.status} for ${url}`);
54
+ }
55
+ const arr = await response.arrayBuffer();
56
+ return Buffer.from(arr);
57
+ }
58
+
59
+ function trimTrailingSlash(input) {
60
+ return String(input || "").replace(/\/+$/, "");
61
+ }
62
+
63
+ function buildBaseUrlCandidates(versionTag) {
64
+ if (process.env.CLOVAPI_CLI_BASE_URL) {
65
+ return [trimTrailingSlash(process.env.CLOVAPI_CLI_BASE_URL)];
66
+ }
67
+
68
+ const bases = [];
69
+ const r2Base = trimTrailingSlash(
70
+ process.env.CLOVAPI_R2_BASE_URL || `https://downloads.clovapi.com/clovapi/${versionTag}`
71
+ );
72
+ if (r2Base) bases.push(r2Base);
73
+ bases.push(`https://github.com/joohw/clovapi/releases/download/${versionTag}`);
74
+ return bases;
75
+ }
76
+
77
+ function parseChecksum(checksumContent, fileName) {
78
+ const line = checksumContent
79
+ .split(/\r?\n/)
80
+ .map((v) => v.trim())
81
+ .find((v) => v.endsWith(` ${fileName}`));
82
+
83
+ if (!line) {
84
+ throw new Error(`checksum not found for ${fileName}`);
85
+ }
86
+
87
+ return line.split(/\s+/)[0];
88
+ }
89
+
90
+ function sha256(buffer) {
91
+ return crypto.createHash("sha256").update(buffer).digest("hex");
92
+ }
93
+
94
+ async function extractArchive(archivePath, archiveName, outDir) {
95
+ if (archiveName.endsWith(".zip")) {
96
+ const zip = new AdmZip(archivePath);
97
+ zip.extractAllTo(outDir, true);
98
+ return;
99
+ }
100
+
101
+ if (archiveName.endsWith(".tar.gz")) {
102
+ await tar.x({
103
+ file: archivePath,
104
+ cwd: outDir,
105
+ gzip: true,
106
+ });
107
+ return;
108
+ }
109
+
110
+ throw new Error(`unsupported archive format: ${archiveName}`);
111
+ }
112
+
113
+ async function main() {
114
+ const { versionTag, archiveName } = getReleaseCoordinates();
115
+ const baseCandidates = buildBaseUrlCandidates(versionTag);
116
+ let checksumBuffer = null;
117
+ let archiveBuffer = null;
118
+ let usedBaseUrl = "";
119
+ let lastError = null;
120
+
121
+ for (const baseUrl of baseCandidates) {
122
+ const checksumUrl = `${baseUrl}/checksums.txt`;
123
+ const archiveUrl = `${baseUrl}/${archiveName}`;
124
+ try {
125
+ console.log(`[clovapi install] trying ${baseUrl}`);
126
+ [checksumBuffer, archiveBuffer] = await Promise.all([
127
+ download(checksumUrl),
128
+ download(archiveUrl),
129
+ ]);
130
+ const expected = parseChecksum(checksumBuffer.toString("utf8"), archiveName);
131
+ const actual = sha256(archiveBuffer);
132
+ if (expected !== actual) {
133
+ throw new Error(`checksum mismatch for ${archiveName}`);
134
+ }
135
+ usedBaseUrl = baseUrl;
136
+ break;
137
+ } catch (error) {
138
+ lastError = error;
139
+ console.warn(`[clovapi install] source failed: ${baseUrl}`);
140
+ }
141
+ }
142
+
143
+ if (!checksumBuffer || !archiveBuffer) {
144
+ throw new Error(`all sources failed: ${lastError ? lastError.message : "unknown error"}`);
145
+ }
146
+
147
+ console.log(`[clovapi install] downloading ${archiveName} from ${usedBaseUrl}`);
148
+
149
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "clovapi-install-"));
150
+ const archivePath = path.join(tmpDir, archiveName);
151
+ fs.writeFileSync(archivePath, archiveBuffer);
152
+
153
+ const vendorDir = path.join(__dirname, "..", "vendor");
154
+ fs.mkdirSync(vendorDir, { recursive: true });
155
+
156
+ await extractArchive(archivePath, archiveName, vendorDir);
157
+
158
+ const exeName = process.platform === "win32" ? "clovapi.exe" : "clovapi";
159
+ const binaryPath = path.join(vendorDir, exeName);
160
+ if (!fs.existsSync(binaryPath)) {
161
+ throw new Error(`binary not found after extraction: ${exeName}`);
162
+ }
163
+
164
+ if (process.platform !== "win32") {
165
+ fs.chmodSync(binaryPath, 0o755);
166
+ }
167
+
168
+ console.log("[clovapi install] install complete");
169
+ }
170
+
171
+ main().catch((err) => {
172
+ fail(err.message);
173
+ });
@@ -0,0 +1 @@
1
+