@clovapi/cli 0.1.17 → 0.1.18
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/bin/clovapi.js +4 -4
- package/package.json +6 -4
- package/scripts/install.js +98 -12
- package/scripts/paths.js +46 -0
package/bin/clovapi.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { spawnSync } = require("node:child_process");
|
|
4
4
|
const fs = require("node:fs");
|
|
5
|
-
const path = require("node:path");
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
const binPath = path.join(__dirname, "..", "vendor", exeName);
|
|
6
|
+
const { cliBinPath, vendorBinPath } = require("../scripts/paths");
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
const binPath = [cliBinPath(), vendorBinPath()].find((candidate) => fs.existsSync(candidate));
|
|
9
|
+
|
|
10
|
+
if (!binPath) {
|
|
11
11
|
console.error("clovapi binary is not installed.");
|
|
12
12
|
console.error("Reinstall with: npm i -g @clovapi/cli");
|
|
13
13
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clovapi/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Install clovapi CLI from GitHub Releases",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/joohw/clovapi.git",
|
|
8
|
-
"directory": "
|
|
8
|
+
"directory": "npm"
|
|
9
9
|
},
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"bin": {
|
|
12
12
|
"clovapi": "bin/clovapi.js"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"postinstall": "node scripts/install.js"
|
|
15
|
+
"postinstall": "node scripts/install.js",
|
|
16
|
+
"test": "node --test scripts/paths.test.js"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"bin",
|
|
19
|
-
"scripts",
|
|
20
|
+
"scripts/install.js",
|
|
21
|
+
"scripts/paths.js",
|
|
20
22
|
"vendor",
|
|
21
23
|
"README.md"
|
|
22
24
|
],
|
package/scripts/install.js
CHANGED
|
@@ -7,6 +7,14 @@ const AdmZip = require("adm-zip");
|
|
|
7
7
|
const tar = require("tar");
|
|
8
8
|
|
|
9
9
|
const pkg = require("../package.json");
|
|
10
|
+
const {
|
|
11
|
+
cliBinDir,
|
|
12
|
+
cliBinPath,
|
|
13
|
+
cliInstallLockPath,
|
|
14
|
+
cliVersionMetaPath,
|
|
15
|
+
exeName,
|
|
16
|
+
vendorBinPath,
|
|
17
|
+
} = require("./paths");
|
|
10
18
|
|
|
11
19
|
const PLATFORM_MAP = {
|
|
12
20
|
darwin: "darwin",
|
|
@@ -111,7 +119,83 @@ async function extractArchive(archivePath, archiveName, outDir) {
|
|
|
111
119
|
throw new Error(`unsupported archive format: ${archiveName}`);
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
function sleep(ms) {
|
|
123
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function withInstallLock(fn) {
|
|
127
|
+
fs.mkdirSync(cliBinDir(), { recursive: true });
|
|
128
|
+
const lockPath = cliInstallLockPath();
|
|
129
|
+
let fd = null;
|
|
130
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
131
|
+
try {
|
|
132
|
+
fd = fs.openSync(lockPath, "wx");
|
|
133
|
+
break;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error && error.code === "EEXIST") {
|
|
136
|
+
await sleep(100);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (fd == null) {
|
|
143
|
+
throw new Error(`timed out waiting for install lock: ${lockPath}`);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
return await fn();
|
|
147
|
+
} finally {
|
|
148
|
+
fs.closeSync(fd);
|
|
149
|
+
try {
|
|
150
|
+
fs.unlinkSync(lockPath);
|
|
151
|
+
} catch {
|
|
152
|
+
// Best-effort cleanup; a future install will retry until the file disappears.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function installBinary(sourcePath, targetPath) {
|
|
158
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
159
|
+
const tmpPath = path.join(path.dirname(targetPath), `.clovapi-install-${process.pid}-${Date.now()}`);
|
|
160
|
+
fs.copyFileSync(sourcePath, tmpPath);
|
|
161
|
+
if (process.platform !== "win32") {
|
|
162
|
+
fs.chmodSync(tmpPath, 0o755);
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
fs.renameSync(tmpPath, targetPath);
|
|
166
|
+
} catch {
|
|
167
|
+
try {
|
|
168
|
+
fs.rmSync(targetPath, { force: true });
|
|
169
|
+
fs.renameSync(tmpPath, targetPath);
|
|
170
|
+
} catch (retryError) {
|
|
171
|
+
fs.rmSync(tmpPath, { force: true });
|
|
172
|
+
throw retryError;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function writeVersionMeta(version) {
|
|
178
|
+
fs.writeFileSync(cliVersionMetaPath(), `${String(version || "").trim()}\n`, { mode: 0o600 });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function installLocalVendorIfPresent() {
|
|
182
|
+
const localBinary = vendorBinPath();
|
|
183
|
+
if (!fs.existsSync(localBinary)) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
await withInstallLock(async () => {
|
|
187
|
+
installBinary(localBinary, cliBinPath());
|
|
188
|
+
writeVersionMeta(pkg.version);
|
|
189
|
+
});
|
|
190
|
+
console.log(`[clovapi install] installed ${cliBinPath()} from local package binary`);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
114
194
|
async function main() {
|
|
195
|
+
if (await installLocalVendorIfPresent()) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
115
199
|
const { versionTag, archiveName } = getReleaseCoordinates();
|
|
116
200
|
const baseCandidates = buildBaseUrlCandidates(versionTag);
|
|
117
201
|
let checksumBuffer = null;
|
|
@@ -151,22 +235,24 @@ async function main() {
|
|
|
151
235
|
const archivePath = path.join(tmpDir, archiveName);
|
|
152
236
|
fs.writeFileSync(archivePath, archiveBuffer);
|
|
153
237
|
|
|
154
|
-
const
|
|
155
|
-
fs.mkdirSync(
|
|
238
|
+
const extractDir = path.join(tmpDir, "extract");
|
|
239
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
240
|
+
await extractArchive(archivePath, archiveName, extractDir);
|
|
156
241
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const binaryPath = path.join(vendorDir, exeName);
|
|
161
|
-
if (!fs.existsSync(binaryPath)) {
|
|
162
|
-
throw new Error(`binary not found after extraction: ${exeName}`);
|
|
242
|
+
const extractedBinaryPath = path.join(extractDir, exeName());
|
|
243
|
+
if (!fs.existsSync(extractedBinaryPath)) {
|
|
244
|
+
throw new Error(`binary not found after extraction: ${exeName()}`);
|
|
163
245
|
}
|
|
164
246
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
247
|
+
await withInstallLock(async () => {
|
|
248
|
+
installBinary(extractedBinaryPath, cliBinPath());
|
|
249
|
+
writeVersionMeta(pkg.version);
|
|
250
|
+
|
|
251
|
+
// Keep a package-local fallback for offline or manually copied npm installs.
|
|
252
|
+
installBinary(extractedBinaryPath, vendorBinPath());
|
|
253
|
+
});
|
|
168
254
|
|
|
169
|
-
console.log(
|
|
255
|
+
console.log(`[clovapi install] installed ${cliBinPath()}`);
|
|
170
256
|
}
|
|
171
257
|
|
|
172
258
|
main().catch((err) => {
|
package/scripts/paths.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const os = require("node:os");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
function configDir() {
|
|
5
|
+
if (process.platform === "win32") {
|
|
6
|
+
const base = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
7
|
+
return path.join(base, "clovapi");
|
|
8
|
+
}
|
|
9
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
10
|
+
if (xdg) return path.join(xdg, "clovapi");
|
|
11
|
+
return path.join(os.homedir(), ".config", "clovapi");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function exeName() {
|
|
15
|
+
return process.platform === "win32" ? "clovapi.exe" : "clovapi";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cliBinDir() {
|
|
19
|
+
return path.join(configDir(), "bin");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cliBinPath() {
|
|
23
|
+
return path.join(cliBinDir(), exeName());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function cliVersionMetaPath() {
|
|
27
|
+
return path.join(cliBinDir(), "version.txt");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cliInstallLockPath() {
|
|
31
|
+
return path.join(cliBinDir(), ".install.lock");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function vendorBinPath() {
|
|
35
|
+
return path.join(__dirname, "..", "vendor", exeName());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
cliBinDir,
|
|
40
|
+
cliBinPath,
|
|
41
|
+
cliInstallLockPath,
|
|
42
|
+
cliVersionMetaPath,
|
|
43
|
+
configDir,
|
|
44
|
+
exeName,
|
|
45
|
+
vendorBinPath,
|
|
46
|
+
};
|