bunosh 0.4.1 → 0.4.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/src/upgrade.js CHANGED
@@ -1,255 +1,335 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
- import { execSync } from 'child_process';
1
+ import { exec, execSync } from 'child_process';
2
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
3
+ import { platform } from 'os';
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+ import { mkdir, chmod } from 'fs/promises';
7
+ import color from 'chalk';
5
8
 
6
- /**
7
- * Detects if bunosh is running as a single executable or npm package
8
- */
9
- export function isExecutable() {
9
+ export async function upgradeCommand(options = {}) {
10
+ const { force = false, check = false } = options;
11
+
10
12
  try {
11
- // Check if we're running from a compiled executable
12
- // In Bun compiled binaries, process.execPath points to the executable
13
- const execPath = process.execPath;
14
- const isCompiledBinary = !execPath.includes('node_modules') &&
15
- !execPath.includes('.bun') &&
16
- (execPath.includes('bunosh') || path.basename(execPath).startsWith('bunosh'));
13
+ const installMethod = detectInstallMethod();
14
+ console.log(`Bunosh is installed via: ${color.bold(installMethod)}`);
17
15
 
18
- return isCompiledBinary;
16
+ if (installMethod === 'bun') {
17
+ if (!check) {
18
+ await upgradeWithBun();
19
+ } else {
20
+ console.log('Will upgrade with: ' + color.bold('bun add -g bunosh'));
21
+ }
22
+ } else if (installMethod === 'npm') {
23
+ if (!check) {
24
+ await upgradeWithNpm();
25
+ } else {
26
+ console.log('Will upgrade with: ' + color.bold('npm update -g bunosh'));
27
+ }
28
+ } else if (installMethod === 'executable') {
29
+ await upgradeExecutable({ force, check });
30
+ } else {
31
+ console.error('Unknown installation method');
32
+ process.exit(1);
33
+ }
19
34
  } catch (error) {
20
- return false;
35
+ console.error(`Upgrade failed: ${error.message}`);
36
+ process.exit(1);
21
37
  }
22
38
  }
23
39
 
24
- /**
25
- * Gets the current executable path
26
- */
27
- export function getExecutablePath() {
28
- if (!isExecutable()) {
29
- throw new Error('Not running as executable');
30
- }
31
- return process.execPath;
32
- }
33
-
34
- /**
35
- * Detects the platform and architecture for download
36
- */
37
- export function getPlatformInfo() {
38
- const platform = process.platform;
39
- const arch = process.arch;
40
-
41
- // Map to GitHub release asset names
42
- switch (platform) {
43
- case 'linux':
44
- if (arch === 'x64' || arch === 'x86_64') {
45
- return { platform: 'linux', arch: 'x64', asset: 'bunosh-linux-x64.tar.gz' };
46
- }
47
- break;
48
- case 'darwin':
49
- if (arch === 'arm64') {
50
- return { platform: 'darwin', arch: 'arm64', asset: 'bunosh-darwin-arm64.tar.gz' };
51
- }
52
- if (arch === 'x64' || arch === 'x86_64') {
53
- return { platform: 'darwin', arch: 'x64', asset: 'bunosh-darwin-x64.tar.gz' };
40
+ function detectInstallMethod() {
41
+ if (process.env.BUNOSH_EXECUTABLE === 'true') {
42
+ return 'executable';
43
+ }
44
+
45
+ const executablePath = process.argv[0];
46
+
47
+ if (executablePath.endsWith('bunosh')) {
48
+ try {
49
+ const buffer = readFileSync(executablePath);
50
+ const header = buffer.subarray(0, 4).toString('hex');
51
+ const binarySignatures = ['7f454c46', 'feedface', 'feedfacf', '4d5a9000'];
52
+
53
+ if (binarySignatures.includes(header)) {
54
+ return 'executable';
54
55
  }
55
- break;
56
- case 'win32':
57
- if (arch === 'x64' || arch === 'x86_64') {
58
- return { platform: 'windows', arch: 'x64', asset: 'bunosh-windows-x64.exe.zip' };
56
+
57
+ const textContent = buffer.toString('utf8');
58
+ if (!textContent.startsWith('#!') && !textContent.includes('node_modules')) {
59
+ return 'executable';
59
60
  }
60
- break;
61
+ } catch (e) {
62
+ return 'executable';
63
+ }
61
64
  }
62
65
 
63
- throw new Error(`Unsupported platform: ${platform} ${arch}`);
64
- }
65
-
66
- /**
67
- * Fetches the latest release info from GitHub
68
- */
69
- export async function getLatestRelease() {
70
- try {
71
- const response = await fetch('https://api.github.com/repos/davertmik/bunosh/releases/latest');
72
- if (!response.ok) {
73
- throw new Error(`GitHub API error: ${response.status}`);
66
+ const invokedVia = process.argv[0];
67
+
68
+ if (invokedVia.endsWith('node') || invokedVia.includes('node.exe')) {
69
+ return 'npm';
70
+ }
71
+
72
+ if (invokedVia.endsWith('bun') || invokedVia.includes('bun.exe')) {
73
+ try {
74
+ const bunGlobalPath = execSync('bun pm -g bin', { encoding: 'utf8' }).trim();
75
+ const bunoshPath = join(bunGlobalPath, 'bunosh');
76
+
77
+ if (existsSync(bunoshPath)) {
78
+ return 'bun';
79
+ }
80
+ } catch (e) {
74
81
  }
75
- return await response.json();
76
- } catch (error) {
77
- throw new Error(`Failed to fetch release info: ${error.message}`);
78
82
  }
79
- }
80
-
81
- /**
82
- * Gets current version from package.json or executable
83
- */
84
- export function getCurrentVersion() {
83
+
85
84
  try {
86
- // Try to read from package.json first
87
- const packagePath = path.join(process.cwd(), 'package.json');
88
- if (fs.existsSync(packagePath)) {
89
- const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
90
- if (pkg.name === 'bunosh') {
91
- return pkg.version;
92
- }
85
+ const npmListOutput = execSync('npm list -g bunosh --depth=0 2>/dev/null || echo "not found"', { encoding: 'utf8' });
86
+ if (npmListOutput.includes('bunosh@') && !npmListOutput.includes('not found')) {
87
+ return 'npm';
93
88
  }
94
89
 
95
- // For executable, version might be embedded or we can try --version
96
- try {
97
- const version = execSync('bunosh --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
98
- return version;
99
- } catch (error) {
100
- // Fallback to unknown
101
- return 'unknown';
90
+ const bunListOutput = execSync('bun pm -g ls 2>/dev/null | grep bunosh || echo "not found"', { encoding: 'utf8' });
91
+ if (bunListOutput.includes('bunosh') && !bunListOutput.includes('not found')) {
92
+ return 'bun';
102
93
  }
103
- } catch (error) {
104
- return 'unknown';
94
+ } catch (e) {
105
95
  }
106
- }
107
-
108
- /**
109
- * Compares version strings (simple semantic version comparison)
110
- */
111
- export function isNewerVersion(latest, current) {
112
- if (current === 'unknown') return true;
113
-
114
- // Remove 'v' prefix if present
115
- const latestClean = latest.replace(/^v/, '');
116
- const currentClean = current.replace(/^v/, '');
117
-
118
- const latestParts = latestClean.split('.').map(n => parseInt(n) || 0);
119
- const currentParts = currentClean.split('.').map(n => parseInt(n) || 0);
120
-
121
- // Pad arrays to same length
122
- const maxLength = Math.max(latestParts.length, currentParts.length);
123
- while (latestParts.length < maxLength) latestParts.push(0);
124
- while (currentParts.length < maxLength) currentParts.push(0);
125
96
 
126
- // Compare each part
127
- for (let i = 0; i < maxLength; i++) {
128
- if (latestParts[i] > currentParts[i]) return true;
129
- if (latestParts[i] < currentParts[i]) return false;
97
+ if (process.argv[0].endsWith('bunosh')) {
98
+ return 'executable';
130
99
  }
131
100
 
132
- return false; // Versions are equal
101
+ return 'unknown';
133
102
  }
134
103
 
135
- /**
136
- * Downloads and extracts the new binary
137
- */
138
- export async function downloadAndInstall(release, platformInfo, executablePath, onProgress) {
139
- const asset = release.assets.find(a => a.name === platformInfo.asset);
140
- if (!asset) {
141
- throw new Error(`No asset found for platform: ${platformInfo.asset}`);
142
- }
143
-
144
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bunosh-upgrade-'));
145
- const downloadPath = path.join(tempDir, asset.name);
104
+ async function upgradeWithBun() {
105
+ console.log('Upgrading with Bun...');
146
106
 
147
- try {
148
- // Download with progress
149
- onProgress?.('Downloading latest release...');
150
- const response = await fetch(asset.browser_download_url);
151
- if (!response.ok) {
152
- throw new Error(`Download failed: ${response.status}`);
153
- }
154
-
155
- const buffer = await response.arrayBuffer();
156
- fs.writeFileSync(downloadPath, Buffer.from(buffer));
157
- onProgress?.('Download complete');
107
+ return new Promise((resolve, reject) => {
108
+ exec('bun add -g bunosh', (error, stdout, stderr) => {
109
+ if (error) {
110
+ reject(new Error(`Bun upgrade failed: ${stderr || error.message}`));
111
+ return;
112
+ }
113
+ console.log(stdout);
114
+ console.log(color.green('Upgrade successful!'));
115
+ resolve();
116
+ });
117
+ });
118
+ }
158
119
 
159
- // Extract and replace
160
- onProgress?.('Extracting and installing...');
161
-
162
- // Create backup of current executable
163
- const backupPath = executablePath + '.backup';
164
- fs.copyFileSync(executablePath, backupPath);
120
+ async function upgradeWithNpm() {
121
+ console.log('Upgrading with npm...');
122
+
123
+ return new Promise((resolve, reject) => {
124
+ exec('npm update -g bunosh', (error, stdout, stderr) => {
125
+ if (error) {
126
+ reject(new Error(`npm upgrade failed: ${stderr || error.message}`));
127
+ return;
128
+ }
129
+ console.log(stdout);
130
+ console.log(color.green('Upgrade successful!'));
131
+ resolve();
132
+ });
133
+ });
134
+ }
165
135
 
166
- if (platformInfo.platform === 'windows') {
167
- // Handle ZIP extraction for Windows
168
- execSync(`powershell -command "Expand-Archive -Path '${downloadPath}' -DestinationPath '${tempDir}' -Force"`);
169
- const extractedExe = path.join(tempDir, 'bunosh-windows-x64.exe');
170
- fs.copyFileSync(extractedExe, executablePath);
171
- } else {
172
- // Handle tar.gz extraction for Unix
173
- const extractDir = path.join(tempDir, 'extracted');
174
- fs.mkdirSync(extractDir);
175
-
176
- execSync(`tar -xzf "${downloadPath}" -C "${extractDir}"`);
136
+ export async function upgradeExecutable(options = {}) {
137
+ const { force = false, check = false } = options;
138
+
139
+ const currentVersion = getCurrentVersion();
140
+ console.log(`Current version: ${color.bold(currentVersion)}`);
141
+
142
+ if (check) {
143
+ console.log('Checking for updates...');
144
+ try {
145
+ const release = await getLatestRelease();
146
+ const latestVersion = release.tag_name;
177
147
 
178
- // Find the executable in extracted files
179
- const extractedFiles = fs.readdirSync(extractDir);
180
- const executableName = extractedFiles.find(f => f.startsWith('bunosh-'));
148
+ console.log(`Latest version: ${color.bold(latestVersion)}`);
181
149
 
182
- if (!executableName) {
183
- throw new Error('Could not find executable in downloaded archive');
150
+ if (isNewerVersion(latestVersion, currentVersion)) {
151
+ console.log(`${color.green('Update available!')} ${currentVersion} ${latestVersion}`);
152
+ console.log('Run ' + color.bold('bunosh upgrade') + ' to update.');
153
+ } else {
154
+ console.log(`${color.green('You are on the latest version!')}`);
184
155
  }
185
-
186
- const extractedPath = path.join(extractDir, executableName);
187
- fs.copyFileSync(extractedPath, executablePath);
188
- fs.chmodSync(executablePath, 0o755);
156
+ } catch (error) {
157
+ console.error(`Failed to check for updates: ${error.message}`);
158
+ process.exit(1);
189
159
  }
190
-
191
- onProgress?.('Installation complete');
192
-
193
- // Clean up
194
- fs.rmSync(tempDir, { recursive: true, force: true });
195
- fs.unlinkSync(backupPath);
196
-
197
- return true;
198
- } catch (error) {
199
- // Restore backup if something went wrong
200
- const backupPath = executablePath + '.backup';
201
- if (fs.existsSync(backupPath)) {
202
- try {
203
- fs.copyFileSync(backupPath, executablePath);
204
- fs.unlinkSync(backupPath);
205
- } catch (restoreError) {
206
- console.error('Failed to restore backup:', restoreError.message);
160
+ return;
161
+ }
162
+
163
+ console.log('Starting upgrade process...');
164
+ console.log();
165
+
166
+ let lastMessage = '';
167
+ const result = await performUpgradeExecutable({
168
+ force,
169
+ onProgress: (message) => {
170
+ if (message !== lastMessage) {
171
+ console.log(` ${message}`);
172
+ lastMessage = message;
207
173
  }
208
174
  }
209
-
210
- // Clean up temp directory
211
- if (fs.existsSync(tempDir)) {
212
- fs.rmSync(tempDir, { recursive: true, force: true });
175
+ });
176
+
177
+ console.log();
178
+ if (result.updated) {
179
+ console.log(`${color.green('Upgrade successful!')}`);
180
+ console.log(` ${result.currentVersion} → ${color.bold(result.latestVersion)}`);
181
+ console.log();
182
+ console.log(`Run ${color.bold('bunosh --version')} to verify the new version.`);
183
+ } else {
184
+ console.log(`${color.green(result.message)}`);
185
+ if (!force) {
186
+ console.log(` Use ${color.bold('--force')} to reinstall the current version.`);
213
187
  }
214
-
215
- throw error;
216
188
  }
217
189
  }
218
190
 
219
- /**
220
- * Main upgrade function
221
- */
222
- export async function upgradeExecutable(options = {}) {
223
- const { force = false, onProgress } = options;
191
+ function getCurrentVersion() {
192
+ try {
193
+ const pkgPath = new URL('../package.json', import.meta.url);
194
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
195
+ return pkg.version;
196
+ } catch (e) {
197
+ return 'unknown';
198
+ }
199
+ }
200
+
201
+ async function getLatestRelease() {
202
+ const response = await fetch('https://api.github.com/repos/davertmik/bunosh/releases/latest', {
203
+ headers: {
204
+ 'User-Agent': 'bunosh'
205
+ }
206
+ });
224
207
 
225
- if (!isExecutable()) {
226
- throw new Error('Upgrade is only available for single executable installations. Use "npm update -g bunosh" for npm installations.');
208
+ if (!response.ok) {
209
+ throw new Error(`GitHub API error: ${response.status}`);
227
210
  }
211
+
212
+ return await response.json();
213
+ }
228
214
 
229
- onProgress?.('Checking for updates...');
215
+ function isNewerVersion(latest, current) {
216
+ const latestParts = latest.replace(/^v/, '').split('.').map(Number);
217
+ const currentParts = current.replace(/^v/, '').split('.').map(Number);
218
+
219
+ for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) {
220
+ const latestPart = latestParts[i] || 0;
221
+ const currentPart = currentParts[i] || 0;
222
+
223
+ if (latestPart > currentPart) return true;
224
+ if (latestPart < currentPart) return false;
225
+ }
226
+
227
+ return false;
228
+ }
229
+
230
+ async function performUpgradeExecutable(options = {}) {
231
+ const { force = false, onProgress = () => {} } = options;
230
232
 
231
- const currentVersion = getCurrentVersion();
232
233
  const release = await getLatestRelease();
233
234
  const latestVersion = release.tag_name;
235
+ const currentVersion = getCurrentVersion();
234
236
 
235
237
  if (!force && !isNewerVersion(latestVersion, currentVersion)) {
236
238
  return {
237
239
  updated: false,
240
+ message: 'Already on latest version',
238
241
  currentVersion,
239
- latestVersion,
240
- message: `Already on latest version: ${currentVersion}`
242
+ latestVersion
241
243
  };
242
244
  }
243
-
244
- const platformInfo = getPlatformInfo();
245
- const executablePath = getExecutablePath();
246
245
 
247
- await downloadAndInstall(release, platformInfo, executablePath, onProgress);
246
+ const platformName = getPlatformName();
247
+ const asset = release.assets.find(a => a.name.includes(platformName));
248
+
249
+ if (!asset) {
250
+ throw new Error(`Unsupported platform: ${platformName}`);
251
+ }
252
+
253
+ onProgress('Downloading update...');
254
+
255
+ const downloadDir = join(homedir(), '.bunosh', 'updates');
256
+ await mkdir(downloadDir, { recursive: true });
257
+
258
+ const downloadPath = join(downloadDir, asset.name);
259
+
260
+ await downloadFile(asset.browser_download_url, downloadPath, onProgress);
261
+
262
+ onProgress('Installing update...');
263
+
264
+ const currentPath = process.argv[0];
265
+ const backupPath = `${currentPath}.backup`;
266
+ if (existsSync(currentPath)) {
267
+ writeFileSync(backupPath, readFileSync(currentPath));
268
+ }
269
+
270
+ writeFileSync(currentPath, readFileSync(downloadPath));
271
+ await chmod(currentPath, '755');
248
272
 
249
273
  return {
250
274
  updated: true,
251
275
  currentVersion,
252
- latestVersion,
253
- message: `Successfully upgraded from ${currentVersion} to ${latestVersion}`
276
+ latestVersion
254
277
  };
278
+ }
279
+
280
+ function getPlatformName() {
281
+ const arch = platform().arch();
282
+ const sysPlatform = platform().toLowerCase();
283
+
284
+ if (sysPlatform === 'darwin' && arch === 'arm64') {
285
+ return 'darwin-arm64';
286
+ }
287
+
288
+ if (sysPlatform === 'darwin' && arch === 'x64') {
289
+ return 'darwin-x64';
290
+ }
291
+
292
+ if (sysPlatform === 'linux' && arch === 'x64') {
293
+ return 'linux-x64';
294
+ }
295
+
296
+ if (sysPlatform === 'win32' && arch === 'x64') {
297
+ return 'windows-x64.exe';
298
+ }
299
+
300
+ throw new Error(`Unsupported platform: ${sysPlatform} ${arch}`);
301
+ }
302
+
303
+ async function downloadFile(url, filePath, onProgress = () => {}) {
304
+ const response = await fetch(url);
305
+
306
+ if (!response.ok) {
307
+ throw new Error(`Download failed: ${response.status}`);
308
+ }
309
+
310
+ const reader = response.body.getReader();
311
+ const contentLength = +response.headers.get('Content-Length');
312
+ let receivedLength = 0;
313
+
314
+ const chunks = [];
315
+ while (true) {
316
+ const { done, value } = await reader.read();
317
+
318
+ if (done) break;
319
+
320
+ chunks.push(value);
321
+ receivedLength += value.length;
322
+
323
+ const progress = Math.round((receivedLength / contentLength) * 100);
324
+ onProgress(`Downloading... ${progress}%`);
325
+ }
326
+
327
+ const chunksAll = new Uint8Array(receivedLength);
328
+ let position = 0;
329
+ for (let chunk of chunks) {
330
+ chunksAll.set(chunk, position);
331
+ position += chunk.length;
332
+ }
333
+
334
+ writeFileSync(filePath, chunksAll);
255
335
  }