@unitsvc/cc-helper 1.0.7 → 1.0.9

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/cc-helper.js CHANGED
@@ -1,411 +1,412 @@
1
- #!/usr/bin/env node
2
-
3
- const https = require('https');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
- const { spawn, execSync } = require('child_process');
8
-
9
- const GITHUB_OWNER = 'next-bin';
10
- const GITHUB_REPO = 'cc-helper';
11
- const CACHE_DIR = path.join(os.homedir(), '.cache', 'cc-helper');
12
- const VERSION_FILE = path.join(CACHE_DIR, 'version.txt');
13
-
14
- // Handle uninstall command directly
15
- function handleUninstall() {
16
- try {
17
- if (fs.existsSync(CACHE_DIR)) {
18
- console.log('Removing cached binaries...');
19
- fs.rmSync(CACHE_DIR, { recursive: true, force: true });
20
- console.log('Cleanup complete!');
21
- } else {
22
- console.log('No cached binaries found.');
23
- }
24
- console.log('\nTo complete uninstallation, run:');
25
- console.log(' npm uninstall -g @unitsvc/cc-helper');
26
- process.exit(0);
27
- } catch (err) {
28
- console.error('Error during uninstall:', err.message);
29
- process.exit(1);
30
- }
31
- }
32
-
33
- // Get latest release version from GitHub API
34
- function getLatestVersion() {
35
- return new Promise((resolve, reject) => {
36
- const options = {
37
- hostname: 'api.github.com',
38
- path: `/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`,
39
- headers: {
40
- 'User-Agent': 'cc-helper-npm-installer',
41
- 'Accept': 'application/vnd.github.v3+json'
42
- }
43
- };
44
-
45
- const req = https.get(options, (res) => {
46
- let data = '';
47
- res.on('data', chunk => data += chunk);
48
- res.on('end', () => {
49
- try {
50
- if (res.statusCode !== 200) {
51
- reject(new Error(`GitHub API returned status ${res.statusCode}`));
52
- return;
53
- }
54
- const release = JSON.parse(data);
55
- const version = release.tag_name.replace(/^v/, '');
56
- resolve(version);
57
- } catch (err) {
58
- reject(new Error(`Failed to parse GitHub response: ${err.message}`));
59
- }
60
- });
61
- });
62
-
63
- req.on('error', reject);
64
- req.setTimeout(10000, () => {
65
- req.destroy();
66
- reject(new Error('GitHub API request timeout'));
67
- });
68
- });
69
- }
70
-
71
- // Get all releases from GitHub API
72
- function getAllReleases() {
73
- return new Promise((resolve, reject) => {
74
- const options = {
75
- hostname: 'api.github.com',
76
- path: `/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases?per_page=10`,
77
- headers: {
78
- 'User-Agent': 'cc-helper-npm-installer',
79
- 'Accept': 'application/vnd.github.v3+json'
80
- }
81
- };
82
-
83
- const req = https.get(options, (res) => {
84
- let data = '';
85
- res.on('data', chunk => data += chunk);
86
- res.on('end', () => {
87
- try {
88
- if (res.statusCode !== 200) {
89
- reject(new Error(`GitHub API returned status ${res.statusCode}`));
90
- return;
91
- }
92
- const releases = JSON.parse(data);
93
- resolve(releases);
94
- } catch (err) {
95
- reject(new Error(`Failed to parse GitHub response: ${err.message}`));
96
- }
97
- });
98
- });
99
-
100
- req.on('error', reject);
101
- req.setTimeout(10000, () => {
102
- req.destroy();
103
- reject(new Error('GitHub API request timeout'));
104
- });
105
- });
106
- }
107
-
108
- // Find a release with binary assets for the given platform
109
- async function findReleaseWithBinary(platform, arch) {
110
- const releases = await getAllReleases();
111
-
112
- for (const release of releases) {
113
- const version = release.tag_name.replace(/^v/, '');
114
- const expectedAsset = platform === 'windows'
115
- ? `cc-helper_${version}_${platform}_${arch}.zip`
116
- : `cc-helper_${version}_${platform}_${arch}.tar.gz`;
117
-
118
- const hasBinary = release.assets && release.assets.some(asset => asset.name === expectedAsset);
119
-
120
- if (hasBinary) {
121
- return version;
122
- }
123
- }
124
-
125
- throw new Error('No release with binary assets found');
126
- }
127
-
128
- function getPlatform() {
129
- const platform = os.platform();
130
- const arch = os.arch();
131
-
132
- const platformMap = {
133
- 'darwin': 'darwin',
134
- 'linux': 'linux',
135
- 'win32': 'windows'
136
- };
137
-
138
- const archMap = {
139
- 'x64': 'amd64',
140
- 'arm64': 'arm64'
141
- };
142
-
143
- const mappedPlatform = platformMap[platform];
144
- const mappedArch = archMap[arch];
145
-
146
- if (!mappedPlatform || !mappedArch) {
147
- throw new Error(`Unsupported platform: ${platform} ${arch}`);
148
- }
149
-
150
- // Windows arm64 not supported by GoReleaser
151
- if (platform === 'win32' && arch === 'arm64') {
152
- throw new Error('Windows arm64 is not supported. Please use Windows amd64 or macOS/Linux arm64.');
153
- }
154
-
155
- return { platform: mappedPlatform, arch: mappedArch };
156
- }
157
-
158
- function getArchiveName(version, platform, arch) {
159
- // GitHub release uses version without 'v' prefix in filename
160
- const ver = version.replace(/^v/, '');
161
- if (platform === 'windows') {
162
- return `cc-helper_${ver}_${platform}_${arch}.zip`;
163
- }
164
- return `cc-helper_${ver}_${platform}_${arch}.tar.gz`;
165
- }
166
-
167
- function getBinaryName(platform) {
168
- return platform === 'windows' ? 'cc-helper.exe' : 'cc-helper';
169
- }
170
-
171
- function getBinaryPath() {
172
- const { platform } = getPlatform();
173
- const binaryName = getBinaryName(platform);
174
- return path.join(CACHE_DIR, binaryName);
175
- }
176
-
177
- function downloadFile(url, dest) {
178
- return new Promise((resolve, reject) => {
179
- const file = fs.createWriteStream(dest);
180
-
181
- const requestUrl = url.startsWith('https://') ? url : 'https://github.com' + url;
182
-
183
- const request = https.get(requestUrl, { headers: { 'User-Agent': 'cc-helper-npm-installer' } }, (response) => {
184
- if (response.statusCode === 302 || response.statusCode === 301) {
185
- file.close();
186
- if (fs.existsSync(dest)) {
187
- fs.unlinkSync(dest);
188
- }
189
- downloadFile(response.headers.location, dest).then(resolve).catch(reject);
190
- return;
191
- }
192
-
193
- if (response.statusCode !== 200) {
194
- file.close();
195
- if (fs.existsSync(dest)) {
196
- fs.unlinkSync(dest);
197
- }
198
- reject(new Error(`Download failed with status ${response.statusCode}: ${url}`));
199
- return;
200
- }
201
-
202
- response.pipe(file);
203
-
204
- file.on('finish', () => {
205
- file.close();
206
- resolve();
207
- });
208
- });
209
-
210
- request.on('error', (err) => {
211
- if (fs.existsSync(dest)) {
212
- fs.unlinkSync(dest);
213
- }
214
- reject(err);
215
- });
216
-
217
- file.on('error', (err) => {
218
- if (fs.existsSync(dest)) {
219
- fs.unlinkSync(dest);
220
- }
221
- reject(err);
222
- });
223
- });
224
- }
225
-
226
- function extractTarGz(tarGzPath, destDir) {
227
- try {
228
- // Use system tar command (available on macOS and Linux)
229
- execSync(`tar -xzf "${tarGzPath}" -C "${destDir}"`, { stdio: 'pipe' });
230
- } catch (err) {
231
- throw new Error(`Failed to extract tar.gz archive: ${err.message}`);
232
- }
233
- }
234
-
235
- function extractZip(zipPath, destDir) {
236
- try {
237
- if (process.platform === 'win32') {
238
- // Use PowerShell on Windows (built-in, no external dependencies)
239
- // Use single-line command to avoid PowerShell multi-line parsing issues
240
- const escapedZipPath = zipPath.replace(/'/g, "''");
241
- const escapedDestDir = destDir.replace(/'/g, "''");
242
- const psCommand = `Expand-Archive -Path '${escapedZipPath}' -DestinationPath '${escapedDestDir}' -Force -ErrorAction Stop`;
243
- execSync(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${psCommand}"`, { stdio: 'pipe' });
244
- } else {
245
- // Unix/Linux/macOS - use unzip
246
- // Check if unzip is available first
247
- try {
248
- execSync('which unzip', { stdio: 'pipe' });
249
- } catch {
250
- throw new Error(
251
- 'unzip command not found. Please install unzip:\n' +
252
- ' Ubuntu/Debian: sudo apt-get install unzip\n' +
253
- ' macOS: brew install unzip\n' +
254
- ' CentOS/RHEL: sudo yum install unzip'
255
- );
256
- }
257
- execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'pipe' });
258
- }
259
- } catch (err) {
260
- if (err.message.includes('unzip command not found')) {
261
- throw err;
262
- }
263
- throw new Error(`Failed to extract zip archive: ${err.message}`);
264
- }
265
- }
266
-
267
- async function extractArchive(archivePath, destDir, platform) {
268
- if (platform === 'windows') {
269
- extractZip(archivePath, destDir);
270
- } else {
271
- extractTarGz(archivePath, destDir);
272
- }
273
- }
274
-
275
- function findBinary(dir, platform) {
276
- const binaryName = getBinaryName(platform);
277
- const files = fs.readdirSync(dir);
278
-
279
- // First check root
280
- const rootBinary = path.join(dir, binaryName);
281
- if (fs.existsSync(rootBinary)) {
282
- return rootBinary;
283
- }
284
-
285
- // Check subdirectories
286
- for (const file of files) {
287
- const fullPath = path.join(dir, file);
288
- const stat = fs.statSync(fullPath);
289
- if (stat.isDirectory()) {
290
- const binaryInSubdir = path.join(fullPath, binaryName);
291
- if (fs.existsSync(binaryInSubdir)) {
292
- return binaryInSubdir;
293
- }
294
- }
295
- }
296
-
297
- throw new Error(`Binary ${binaryName} not found in archive`);
298
- }
299
-
300
- async function downloadAndInstall(version, platform, arch, binaryPath) {
301
- const archiveName = getArchiveName(version, platform, arch);
302
- const tagVersion = version.startsWith('v') ? version : `v${version}`;
303
- const downloadUrl = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download/${tagVersion}/${archiveName}`;
304
- const archivePath = path.join(CACHE_DIR, archiveName);
305
-
306
- console.log(`Downloading cc-helper ${version} for ${platform}-${arch}...`);
307
-
308
- try {
309
- await downloadFile(downloadUrl, archivePath);
310
-
311
- // Extract archive
312
- console.log('Extracting archive...');
313
- const tempDir = path.join(CACHE_DIR, 'temp_extract_' + Date.now());
314
- fs.mkdirSync(tempDir, { recursive: true });
315
-
316
- await extractArchive(archivePath, tempDir, platform);
317
-
318
- // Find and move binary
319
- const extractedBinaryPath = findBinary(tempDir, platform);
320
-
321
- // Remove old binary if exists
322
- if (fs.existsSync(binaryPath)) {
323
- fs.unlinkSync(binaryPath);
324
- }
325
-
326
- // Move binary to cache dir
327
- fs.copyFileSync(extractedBinaryPath, binaryPath);
328
- fs.chmodSync(binaryPath, 0o755);
329
-
330
- // Clean up
331
- fs.rmSync(tempDir, { recursive: true, force: true });
332
- fs.unlinkSync(archivePath);
333
-
334
- // Save version
335
- fs.writeFileSync(VERSION_FILE, version);
336
-
337
- console.log('Download complete!');
338
- } catch (err) {
339
- // Clean up on failure
340
- if (fs.existsSync(archivePath)) {
341
- fs.unlinkSync(archivePath);
342
- }
343
- throw err;
344
- }
345
- }
346
-
347
- async function ensureBinary() {
348
- const binaryPath = getBinaryPath();
349
-
350
- // If binary exists, use it directly (postinstall already handled version check)
351
- if (fs.existsSync(binaryPath)) {
352
- return binaryPath;
353
- }
354
-
355
- // Binary doesn't exist, find a compatible release and download
356
- console.log('Finding compatible release...');
357
- const { platform, arch } = getPlatform();
358
- const compatibleVersion = await findReleaseWithBinary(platform, arch);
359
-
360
- // Create cache directory
361
- if (!fs.existsSync(CACHE_DIR)) {
362
- fs.mkdirSync(CACHE_DIR, { recursive: true });
363
- }
364
-
365
- // Download and install
366
- await downloadAndInstall(compatibleVersion, platform, arch, binaryPath);
367
-
368
- return binaryPath;
369
- }
370
-
371
- async function main() {
372
- // Handle uninstall command
373
- const args = process.argv.slice(2);
374
- if (args.length > 0 && args[0] === 'uninstall') {
375
- handleUninstall();
376
- return;
377
- }
378
-
379
- try {
380
- const binaryPath = await ensureBinary();
381
-
382
- // Run the binary with all arguments
383
- // Windows requires shell: true for .exe files
384
- const isWindows = process.platform === 'win32';
385
- const child = spawn(binaryPath, args, {
386
- stdio: 'inherit',
387
- detached: false,
388
- shell: isWindows
389
- });
390
-
391
- child.on('close', (code) => {
392
- process.exit(code);
393
- });
394
-
395
- child.on('error', (err) => {
396
- console.error('Failed to start cc-helper:', err.message);
397
- process.exit(1);
398
- });
399
- } catch (err) {
400
- console.error('Error:', err.message);
401
- process.exit(1);
402
- }
403
- }
404
-
405
- // Export for use as module
406
- module.exports = { main, ensureBinary, getBinaryPath, getPlatform, getLatestVersion };
407
-
408
- // Run if called directly
409
- if (require.main === module) {
410
- main();
411
- }
1
+ #!/usr/bin/env node
2
+
3
+ const https = require('https');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { spawn, execSync } = require('child_process');
8
+
9
+ const GITHUB_OWNER = 'next-bin';
10
+ const GITHUB_REPO = 'cc-helper';
11
+ const CACHE_DIR = path.join(os.homedir(), '.cache', 'cc-helper');
12
+ const VERSION_FILE = path.join(CACHE_DIR, 'version.txt');
13
+
14
+ // Handle uninstall command directly
15
+ function handleUninstall() {
16
+ try {
17
+ if (fs.existsSync(CACHE_DIR)) {
18
+ console.log('Removing cached binaries...');
19
+ fs.rmSync(CACHE_DIR, { recursive: true, force: true });
20
+ console.log('Cleanup complete!');
21
+ } else {
22
+ console.log('No cached binaries found.');
23
+ }
24
+ console.log('\nTo complete uninstallation, run:');
25
+ console.log(' npm uninstall -g @unitsvc/cc-helper');
26
+ process.exit(0);
27
+ } catch (err) {
28
+ console.error('Error during uninstall:', err.message);
29
+ process.exit(1);
30
+ }
31
+ }
32
+
33
+ // Get latest release version from GitHub API
34
+ function getLatestVersion() {
35
+ return new Promise((resolve, reject) => {
36
+ const options = {
37
+ hostname: 'api.github.com',
38
+ path: `/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`,
39
+ headers: {
40
+ 'User-Agent': 'cc-helper-npm-installer',
41
+ 'Accept': 'application/vnd.github.v3+json'
42
+ }
43
+ };
44
+
45
+ const req = https.get(options, (res) => {
46
+ let data = '';
47
+ res.on('data', chunk => data += chunk);
48
+ res.on('end', () => {
49
+ try {
50
+ if (res.statusCode !== 200) {
51
+ reject(new Error(`GitHub API returned status ${res.statusCode}`));
52
+ return;
53
+ }
54
+ const release = JSON.parse(data);
55
+ const version = release.tag_name.replace(/^v/, '');
56
+ resolve(version);
57
+ } catch (err) {
58
+ reject(new Error(`Failed to parse GitHub response: ${err.message}`));
59
+ }
60
+ });
61
+ });
62
+
63
+ req.on('error', reject);
64
+ req.setTimeout(10000, () => {
65
+ req.destroy();
66
+ reject(new Error('GitHub API request timeout'));
67
+ });
68
+ });
69
+ }
70
+
71
+ // Get all releases from GitHub API
72
+ function getAllReleases() {
73
+ return new Promise((resolve, reject) => {
74
+ const options = {
75
+ hostname: 'api.github.com',
76
+ path: `/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases?per_page=10`,
77
+ headers: {
78
+ 'User-Agent': 'cc-helper-npm-installer',
79
+ 'Accept': 'application/vnd.github.v3+json'
80
+ }
81
+ };
82
+
83
+ const req = https.get(options, (res) => {
84
+ let data = '';
85
+ res.on('data', chunk => data += chunk);
86
+ res.on('end', () => {
87
+ try {
88
+ if (res.statusCode !== 200) {
89
+ reject(new Error(`GitHub API returned status ${res.statusCode}`));
90
+ return;
91
+ }
92
+ const releases = JSON.parse(data);
93
+ resolve(releases);
94
+ } catch (err) {
95
+ reject(new Error(`Failed to parse GitHub response: ${err.message}`));
96
+ }
97
+ });
98
+ });
99
+
100
+ req.on('error', reject);
101
+ req.setTimeout(10000, () => {
102
+ req.destroy();
103
+ reject(new Error('GitHub API request timeout'));
104
+ });
105
+ });
106
+ }
107
+
108
+ // Find a release with binary assets for the given platform
109
+ async function findReleaseWithBinary(platform, arch) {
110
+ const releases = await getAllReleases();
111
+
112
+ for (const release of releases) {
113
+ const version = release.tag_name.replace(/^v/, '');
114
+ const expectedAsset = platform === 'windows'
115
+ ? `cc-helper_${version}_${platform}_${arch}.zip`
116
+ : `cc-helper_${version}_${platform}_${arch}.tar.gz`;
117
+
118
+ const hasBinary = release.assets && release.assets.some(asset => asset.name === expectedAsset);
119
+
120
+ if (hasBinary) {
121
+ return version;
122
+ }
123
+ }
124
+
125
+ throw new Error('No release with binary assets found');
126
+ }
127
+
128
+ function getPlatform() {
129
+ const platform = os.platform();
130
+ const arch = os.arch();
131
+
132
+ const platformMap = {
133
+ 'darwin': 'darwin',
134
+ 'linux': 'linux',
135
+ 'win32': 'windows'
136
+ };
137
+
138
+ const archMap = {
139
+ 'x64': 'amd64',
140
+ 'arm64': 'arm64'
141
+ };
142
+
143
+ const mappedPlatform = platformMap[platform];
144
+ const mappedArch = archMap[arch];
145
+
146
+ if (!mappedPlatform || !mappedArch) {
147
+ throw new Error(`Unsupported platform: ${platform} ${arch}`);
148
+ }
149
+
150
+ // Windows arm64 not supported by GoReleaser
151
+ if (platform === 'win32' && arch === 'arm64') {
152
+ throw new Error('Windows arm64 is not supported. Please use Windows amd64 or macOS/Linux arm64.');
153
+ }
154
+
155
+ return { platform: mappedPlatform, arch: mappedArch };
156
+ }
157
+
158
+ function getArchiveName(version, platform, arch) {
159
+ // GitHub release uses version without 'v' prefix in filename
160
+ const ver = version.replace(/^v/, '');
161
+ if (platform === 'windows') {
162
+ return `cc-helper_${ver}_${platform}_${arch}.zip`;
163
+ }
164
+ return `cc-helper_${ver}_${platform}_${arch}.tar.gz`;
165
+ }
166
+
167
+ function getBinaryName(platform) {
168
+ return platform === 'windows' ? 'cc-helper.exe' : 'cc-helper';
169
+ }
170
+
171
+ function getBinaryPath() {
172
+ const { platform } = getPlatform();
173
+ const binaryName = getBinaryName(platform);
174
+ return path.join(CACHE_DIR, binaryName);
175
+ }
176
+
177
+ function downloadFile(url, dest) {
178
+ return new Promise((resolve, reject) => {
179
+ const file = fs.createWriteStream(dest);
180
+
181
+ const requestUrl = url.startsWith('https://') ? url : 'https://github.com' + url;
182
+
183
+ const request = https.get(requestUrl, { headers: { 'User-Agent': 'cc-helper-npm-installer' } }, (response) => {
184
+ if (response.statusCode === 302 || response.statusCode === 301) {
185
+ file.close();
186
+ if (fs.existsSync(dest)) {
187
+ fs.unlinkSync(dest);
188
+ }
189
+ downloadFile(response.headers.location, dest).then(resolve).catch(reject);
190
+ return;
191
+ }
192
+
193
+ if (response.statusCode !== 200) {
194
+ file.close();
195
+ if (fs.existsSync(dest)) {
196
+ fs.unlinkSync(dest);
197
+ }
198
+ reject(new Error(`Download failed with status ${response.statusCode}: ${url}`));
199
+ return;
200
+ }
201
+
202
+ response.pipe(file);
203
+
204
+ file.on('finish', () => {
205
+ file.close();
206
+ resolve();
207
+ });
208
+ });
209
+
210
+ request.on('error', (err) => {
211
+ if (fs.existsSync(dest)) {
212
+ fs.unlinkSync(dest);
213
+ }
214
+ reject(err);
215
+ });
216
+
217
+ file.on('error', (err) => {
218
+ if (fs.existsSync(dest)) {
219
+ fs.unlinkSync(dest);
220
+ }
221
+ reject(err);
222
+ });
223
+ });
224
+ }
225
+
226
+ function extractTarGz(tarGzPath, destDir) {
227
+ try {
228
+ // Use system tar command (available on macOS and Linux)
229
+ execSync(`tar -xzf "${tarGzPath}" -C "${destDir}"`, { stdio: 'pipe' });
230
+ } catch (err) {
231
+ throw new Error(`Failed to extract tar.gz archive: ${err.message}`);
232
+ }
233
+ }
234
+
235
+ function extractZip(zipPath, destDir) {
236
+ try {
237
+ if (process.platform === 'win32') {
238
+ // Use PowerShell on Windows (built-in, no external dependencies)
239
+ // Use single-line command to avoid PowerShell multi-line parsing issues
240
+ // Disable progress bar to avoid Write-Progress IndexOutOfRangeException bug in PowerShell 5.1
241
+ const escapedZipPath = zipPath.replace(/'/g, "''");
242
+ const escapedDestDir = destDir.replace(/'/g, "''");
243
+ const psCommand = `$ProgressPreference = 'SilentlyContinue'; Expand-Archive -Path '${escapedZipPath}' -DestinationPath '${escapedDestDir}' -Force -ErrorAction Stop`;
244
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${psCommand}"`, { stdio: 'pipe' });
245
+ } else {
246
+ // Unix/Linux/macOS - use unzip
247
+ // Check if unzip is available first
248
+ try {
249
+ execSync('which unzip', { stdio: 'pipe' });
250
+ } catch {
251
+ throw new Error(
252
+ 'unzip command not found. Please install unzip:\n' +
253
+ ' Ubuntu/Debian: sudo apt-get install unzip\n' +
254
+ ' macOS: brew install unzip\n' +
255
+ ' CentOS/RHEL: sudo yum install unzip'
256
+ );
257
+ }
258
+ execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'pipe' });
259
+ }
260
+ } catch (err) {
261
+ if (err.message.includes('unzip command not found')) {
262
+ throw err;
263
+ }
264
+ throw new Error(`Failed to extract zip archive: ${err.message}`);
265
+ }
266
+ }
267
+
268
+ async function extractArchive(archivePath, destDir, platform) {
269
+ if (platform === 'windows') {
270
+ extractZip(archivePath, destDir);
271
+ } else {
272
+ extractTarGz(archivePath, destDir);
273
+ }
274
+ }
275
+
276
+ function findBinary(dir, platform) {
277
+ const binaryName = getBinaryName(platform);
278
+ const files = fs.readdirSync(dir);
279
+
280
+ // First check root
281
+ const rootBinary = path.join(dir, binaryName);
282
+ if (fs.existsSync(rootBinary)) {
283
+ return rootBinary;
284
+ }
285
+
286
+ // Check subdirectories
287
+ for (const file of files) {
288
+ const fullPath = path.join(dir, file);
289
+ const stat = fs.statSync(fullPath);
290
+ if (stat.isDirectory()) {
291
+ const binaryInSubdir = path.join(fullPath, binaryName);
292
+ if (fs.existsSync(binaryInSubdir)) {
293
+ return binaryInSubdir;
294
+ }
295
+ }
296
+ }
297
+
298
+ throw new Error(`Binary ${binaryName} not found in archive`);
299
+ }
300
+
301
+ async function downloadAndInstall(version, platform, arch, binaryPath) {
302
+ const archiveName = getArchiveName(version, platform, arch);
303
+ const tagVersion = version.startsWith('v') ? version : `v${version}`;
304
+ const downloadUrl = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download/${tagVersion}/${archiveName}`;
305
+ const archivePath = path.join(CACHE_DIR, archiveName);
306
+
307
+ console.log(`Downloading cc-helper ${version} for ${platform}-${arch}...`);
308
+
309
+ try {
310
+ await downloadFile(downloadUrl, archivePath);
311
+
312
+ // Extract archive
313
+ console.log('Extracting archive...');
314
+ const tempDir = path.join(CACHE_DIR, 'temp_extract_' + Date.now());
315
+ fs.mkdirSync(tempDir, { recursive: true });
316
+
317
+ await extractArchive(archivePath, tempDir, platform);
318
+
319
+ // Find and move binary
320
+ const extractedBinaryPath = findBinary(tempDir, platform);
321
+
322
+ // Remove old binary if exists
323
+ if (fs.existsSync(binaryPath)) {
324
+ fs.unlinkSync(binaryPath);
325
+ }
326
+
327
+ // Move binary to cache dir
328
+ fs.copyFileSync(extractedBinaryPath, binaryPath);
329
+ fs.chmodSync(binaryPath, 0o755);
330
+
331
+ // Clean up
332
+ fs.rmSync(tempDir, { recursive: true, force: true });
333
+ fs.unlinkSync(archivePath);
334
+
335
+ // Save version
336
+ fs.writeFileSync(VERSION_FILE, version);
337
+
338
+ console.log('Download complete!');
339
+ } catch (err) {
340
+ // Clean up on failure
341
+ if (fs.existsSync(archivePath)) {
342
+ fs.unlinkSync(archivePath);
343
+ }
344
+ throw err;
345
+ }
346
+ }
347
+
348
+ async function ensureBinary() {
349
+ const binaryPath = getBinaryPath();
350
+
351
+ // If binary exists, use it directly (postinstall already handled version check)
352
+ if (fs.existsSync(binaryPath)) {
353
+ return binaryPath;
354
+ }
355
+
356
+ // Binary doesn't exist, find a compatible release and download
357
+ console.log('Finding compatible release...');
358
+ const { platform, arch } = getPlatform();
359
+ const compatibleVersion = await findReleaseWithBinary(platform, arch);
360
+
361
+ // Create cache directory
362
+ if (!fs.existsSync(CACHE_DIR)) {
363
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
364
+ }
365
+
366
+ // Download and install
367
+ await downloadAndInstall(compatibleVersion, platform, arch, binaryPath);
368
+
369
+ return binaryPath;
370
+ }
371
+
372
+ async function main() {
373
+ // Handle uninstall command
374
+ const args = process.argv.slice(2);
375
+ if (args.length > 0 && args[0] === 'uninstall') {
376
+ handleUninstall();
377
+ return;
378
+ }
379
+
380
+ try {
381
+ const binaryPath = await ensureBinary();
382
+
383
+ // Run the binary with all arguments
384
+ // Windows requires shell: true for .exe files
385
+ const isWindows = process.platform === 'win32';
386
+ const child = spawn(binaryPath, args, {
387
+ stdio: 'inherit',
388
+ detached: false,
389
+ shell: isWindows
390
+ });
391
+
392
+ child.on('close', (code) => {
393
+ process.exit(code);
394
+ });
395
+
396
+ child.on('error', (err) => {
397
+ console.error('Failed to start cc-helper:', err.message);
398
+ process.exit(1);
399
+ });
400
+ } catch (err) {
401
+ console.error('Error:', err.message);
402
+ process.exit(1);
403
+ }
404
+ }
405
+
406
+ // Export for use as module
407
+ module.exports = { main, ensureBinary, getBinaryPath, getPlatform, getLatestVersion };
408
+
409
+ // Run if called directly
410
+ if (require.main === module) {
411
+ main();
412
+ }