@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 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 exeName = process.platform === "win32" ? "clovapi.exe" : "clovapi";
8
- const binPath = path.join(__dirname, "..", "vendor", exeName);
6
+ const { cliBinPath, vendorBinPath } = require("../scripts/paths");
9
7
 
10
- if (!fs.existsSync(binPath)) {
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.17",
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": "switcher/npm"
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
  ],
@@ -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 vendorDir = path.join(__dirname, "..", "vendor");
155
- fs.mkdirSync(vendorDir, { recursive: true });
238
+ const extractDir = path.join(tmpDir, "extract");
239
+ fs.mkdirSync(extractDir, { recursive: true });
240
+ await extractArchive(archivePath, archiveName, extractDir);
156
241
 
157
- await extractArchive(archivePath, archiveName, vendorDir);
158
-
159
- const exeName = process.platform === "win32" ? "clovapi.exe" : "clovapi";
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
- if (process.platform !== "win32") {
166
- fs.chmodSync(binaryPath, 0o755);
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("[clovapi install] install complete");
255
+ console.log(`[clovapi install] installed ${cliBinPath()}`);
170
256
  }
171
257
 
172
258
  main().catch((err) => {
@@ -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
+ };