magicbell-cli 1.2.0 → 1.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # magicbell-cli
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#7414](https://github.com/magicbell/magicbell/pull/7414) [`fb44a37`](https://github.com/magicbell/magicbell/commit/fb44a37b479b32c08501b8f653e323a0836cc3cc) Thanks [@smeijer](https://github.com/smeijer)! - fix windows installer
8
+
9
+ ## 1.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#7410](https://github.com/magicbell/magicbell/pull/7410) [`0554378`](https://github.com/magicbell/magicbell/commit/05543786fcbb67db85079f3a5a920bcfdd54aea8) Thanks [@smeijer](https://github.com/smeijer)! - regen
14
+
3
15
  ## 1.2.0
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -1,20 +1,16 @@
1
1
  {
2
2
  "name": "magicbell-cli",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "MagicBell CLI",
5
5
  "type": "module",
6
6
  "dependencies": {
7
- "node-fetch": "^3.3.2",
8
- "tar": "^7.5.2"
7
+ "tar": "^7.5.2",
8
+ "unzipper": "^0.11.4"
9
9
  },
10
10
  "bin": {
11
- "magicbell": "bin/magicbell"
11
+ "magicbell": "src/cli.js"
12
12
  },
13
13
  "scripts": {
14
- "postinstall": "node src/install.js install",
15
- "preuninstall": "node src/install.js uninstall",
16
- "prepack": "npx -y pinst --enable",
17
- "postpack": "npx -y pinst --disable",
18
14
  "test": "echo \"Error: no test specified\" && exit 1"
19
15
  },
20
16
  "author": "MagicBell",
@@ -26,6 +22,6 @@
26
22
  "goBinary": {
27
23
  "name": "magicbell",
28
24
  "path": "./bin",
29
- "url": "https://github.com/magicbell/homebrew-tap/releases/download/v{{version}}/magicbell-cli_{{platform}}_{{arch}}.tar.gz"
25
+ "url": "https://github.com/magicbell/homebrew-tap/releases/download/v{{version}}/magicbell-cli_{{platform}}_{{arch}}{{archive_ext}}"
30
26
  }
31
27
  }
package/src/cli.js ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ import { installBinary } from './install.js';
8
+ import { createDebugLogger } from './log.js';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const BIN_DIR = path.resolve(__dirname, '../bin');
12
+ const BINARY_NAME = process.platform === 'win32' ? 'magicbell.exe' : 'magicbell';
13
+ const BINARY_PATH = path.join(BIN_DIR, BINARY_NAME);
14
+ const SPAWN_RETRY_ATTEMPTS = 5;
15
+ const SPAWN_RETRY_DELAY_MS = 200;
16
+ const debug = createDebugLogger('cli');
17
+
18
+ async function ensureBinary() {
19
+ if (fs.existsSync(BINARY_PATH)) {
20
+ debug('Binary already present at', BINARY_PATH);
21
+ return;
22
+ }
23
+
24
+ debug('Binary missing, installing');
25
+ await installBinary();
26
+ debug('Installation finished');
27
+
28
+ if (!fs.existsSync(BINARY_PATH)) {
29
+ throw new Error(`MagicBell binary is missing at ${BINARY_PATH} even after installation.`);
30
+ }
31
+ }
32
+
33
+ function delay(ms) {
34
+ return new Promise((resolve) => {
35
+ setTimeout(resolve, ms);
36
+ });
37
+ }
38
+
39
+ async function spawnWithRetry(args) {
40
+ for (let attempt = 1; attempt <= SPAWN_RETRY_ATTEMPTS; attempt += 1) {
41
+ try {
42
+ debug('Spawning binary', { args, attempt });
43
+ return spawn(BINARY_PATH, args, {
44
+ stdio: 'inherit',
45
+ });
46
+ } catch (error) {
47
+ if (error && error.code === 'EBUSY' && attempt < SPAWN_RETRY_ATTEMPTS) {
48
+ debug('Spawn failed with EBUSY, retrying', { attempt, delayMs: SPAWN_RETRY_DELAY_MS * attempt });
49
+ await delay(SPAWN_RETRY_DELAY_MS * attempt);
50
+ continue;
51
+ }
52
+
53
+ debug('Spawn failed with fatal error', error);
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ throw new Error('Unable to spawn MagicBell CLI binary after multiple attempts.');
59
+ }
60
+
61
+ async function run() {
62
+ try {
63
+ await ensureBinary();
64
+ debug('Binary ready, invoking CLI', { args: process.argv.slice(2) });
65
+ } catch (err) {
66
+ console.error(err instanceof Error ? err.message : err);
67
+ process.exit(1);
68
+ }
69
+
70
+ let child;
71
+ try {
72
+ child = await spawnWithRetry(process.argv.slice(2));
73
+ } catch (error) {
74
+ const message = error instanceof Error ? error.message : error;
75
+ console.error('Failed to start the MagicBell binary:', message);
76
+ process.exit(1);
77
+ }
78
+
79
+ child.on('error', (error) => {
80
+ console.error('Failed to start the MagicBell binary:', error.message);
81
+ process.exit(1);
82
+ });
83
+
84
+ child.on('exit', (code, signal) => {
85
+ debug('Child exited', { code, signal });
86
+ if (signal) {
87
+ process.kill(process.pid, signal);
88
+ return;
89
+ }
90
+
91
+ process.exit(code ?? 0);
92
+ });
93
+ }
94
+
95
+ run();
package/src/install.js CHANGED
@@ -1,18 +1,22 @@
1
- #!/usr/bin/env node
2
- 'use strict';
1
+ // based on https://github.com/Nelwhix/go-npm/blob/main/src/index.js
2
+ // adjustments made so bin is installed to ./bin,
3
+ // and used via a lazy-load mechanism instead of during postinstall
3
4
 
4
- /* eslint-disable no-console */
5
-
6
- // copy from https://github.com/Nelwhix/go-npm/blob/main/src/index.js
7
- // adjustments made so bin is installed to ./bin instead of node global .bin folder
8
-
9
- import { exec } from 'child_process';
10
5
  import fs from 'fs';
11
- import fetch from 'node-fetch';
12
6
  import path from 'path';
7
+ import { pipeline, Readable } from 'stream';
13
8
  import * as tar from 'tar';
9
+ import unzipper from 'unzipper';
10
+ import { fileURLToPath } from 'url';
14
11
  import zlib from 'zlib';
15
12
 
13
+ import { createDebugLogger, isDebugEnabled } from './log.js';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
18
+ const debug = createDebugLogger('install');
19
+
16
20
  // Mapping from Node's `process.arch` to Golang's `$GOARCH`
17
21
  const ARCH_MAPPING = {
18
22
  ia32: '386',
@@ -29,43 +33,7 @@ const PLATFORM_MAPPING = {
29
33
  freebsd: 'freebsd',
30
34
  };
31
35
 
32
- // to get the path where npm binaries are stored
33
- function getInstallationPath(callback) {
34
- exec('npm --v', (err, stdout, _stderr) => {
35
- const npmVersion = parseFloat(stdout.trim());
36
-
37
- // npm bin was deprecated after v9 https://github.blog/changelog/2022-10-24-npm-v9-0-0-released/
38
- if (npmVersion < 9) {
39
- exec('npm bin -g', (err, stdout, stderr) => {
40
- let dir = null;
41
-
42
- if (err || stderr || !stdout || stdout.length === 0) {
43
- throw new Error('Could not get installation path');
44
- } else {
45
- dir = stdout.trim();
46
- }
47
-
48
- fs.mkdirSync(dir, { recursive: true });
49
- callback(null, dir);
50
- });
51
- } else {
52
- exec('npm prefix -g', (err, stdout, stderr) => {
53
- let dir = null;
54
-
55
- if (err || stderr || !stdout || stdout.length === 0) {
56
- throw new Error('Could not get installation path');
57
- } else {
58
- dir = stdout.trim() + '/bin';
59
- }
60
-
61
- fs.mkdirSync(dir, { recursive: true });
62
- callback(null, dir);
63
- });
64
- }
65
- });
66
- }
67
-
68
- function verifyAndPlaceBinary(binName, binPath, callback) {
36
+ function verifyAndPlaceBinary(binName, binPath) {
69
37
  const targetPath = path.join(binPath, binName);
70
38
 
71
39
  if (!fs.existsSync(targetPath)) {
@@ -77,8 +45,6 @@ function verifyAndPlaceBinary(binName, binPath, callback) {
77
45
  } catch (err) {
78
46
  // ignore on Windows
79
47
  }
80
-
81
- callback();
82
48
  }
83
49
 
84
50
  function validateConfiguration(packageJson) {
@@ -103,39 +69,35 @@ function validateConfiguration(packageJson) {
103
69
  }
104
70
  }
105
71
 
106
- function parsePackageJson() {
72
+ function loadConfiguration() {
73
+ debug('Loading configuration', { platform: process.platform, arch: process.arch });
74
+ const isWindows = process.platform === 'win32';
107
75
  if (!(process.arch in ARCH_MAPPING)) {
108
- console.error('Installation is not supported for this architecture: ' + process.arch);
109
- return;
76
+ throw new Error('Installation is not supported for this architecture: ' + process.arch);
110
77
  }
111
78
 
112
79
  if (!(process.platform in PLATFORM_MAPPING)) {
113
- console.error('Installation is not supported for this platform: ' + process.platform);
114
- return;
80
+ throw new Error('Installation is not supported for this platform: ' + process.platform);
115
81
  }
116
82
 
117
- const packageJsonPath = path.join('.', 'package.json');
83
+ const packageJsonPath = path.join(PACKAGE_ROOT, 'package.json');
118
84
  if (!fs.existsSync(packageJsonPath)) {
119
- console.error(
120
- 'Unable to find package.json. ' + 'Please run this script at root of the package you want to be installed',
121
- );
122
- return;
85
+ throw new Error('Unable to find package.json. Please run this script at the root of the package.');
123
86
  }
124
87
 
125
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath));
88
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
126
89
  const error = validateConfiguration(packageJson);
127
90
  if (error && error.length > 0) {
128
- console.error('Invalid package.json: ' + error);
129
- return;
91
+ throw new Error('Invalid package.json: ' + error);
130
92
  }
131
93
 
132
94
  let binName = packageJson.goBinary.name;
133
- const binPath = packageJson.goBinary.path;
95
+ const binPath = path.resolve(PACKAGE_ROOT, packageJson.goBinary.path);
134
96
  let url = packageJson.goBinary.url;
135
97
  let version = packageJson.goBinary.binaryVersion || packageJson.version;
136
98
  if (version[0] === 'v') version = version.substr(1);
137
99
 
138
- if (process.platform === 'win32') {
100
+ if (isWindows) {
139
101
  binName += '.exe';
140
102
  }
141
103
 
@@ -144,12 +106,16 @@ function parsePackageJson() {
144
106
  url = url.replace(/{{platform}}/g, PLATFORM_MAPPING[process.platform]);
145
107
  url = url.replace(/{{version}}/g, version);
146
108
  url = url.replace(/{{bin_name}}/g, binName);
109
+ const archiveExtension = isWindows ? '.zip' : '.tar.gz';
110
+ url = url.replace(/{{archive_ext}}/g, archiveExtension);
111
+ debug('Resolved download URL', url);
147
112
 
148
113
  return {
149
- binName: binName,
150
- binPath: binPath,
151
- url: url,
152
- version: version,
114
+ binName,
115
+ binPath,
116
+ url,
117
+ version,
118
+ archiveType: isWindows ? 'zip' : 'tar',
153
119
  };
154
120
  }
155
121
 
@@ -161,66 +127,65 @@ function parsePackageJson() {
161
127
  *
162
128
  * See: https://docs.npmjs.com/files/package.json#bin
163
129
  */
164
- const INVALID_INPUT = 'Invalid inputs';
165
- function install(callback) {
166
- const options = parsePackageJson();
167
- if (!options) {
168
- throw new Error(INVALID_INPUT);
169
- }
130
+ export function installBinary() {
131
+ const options = loadConfiguration();
170
132
 
171
133
  fs.mkdirSync(options.binPath, { recursive: true });
172
- const ungz = zlib.createGunzip();
173
- const untar = tar.x({ cwd: options.binPath });
174
-
175
- // First we will Un-GZip, then we will untar. So once untar is completed,
176
- // binary is downloaded into `binPath`. Verify the binary and call it good
177
- untar.on('end', () => {
178
- verifyAndPlaceBinary(options.binName, options.binPath, callback);
179
- });
180
-
181
- console.log('Downloading from URL: ' + options.url);
182
-
183
- fetch(options.url).then((res) => {
184
- if (!res.ok) {
185
- throw new Error('Error downloading binary. HTTP Status Code: ' + res.status);
186
- }
187
-
188
- res.body.pipe(ungz).pipe(untar);
189
- });
190
- }
134
+ debug('Prepared bin path', options.binPath);
135
+
136
+ return new Promise((resolve, reject) => {
137
+ let settled = false;
138
+ const finish = (action, value) => {
139
+ if (settled) return;
140
+ settled = true;
141
+ action(value);
142
+ };
143
+
144
+ const handleError = (err) => {
145
+ if (isDebugEnabled) {
146
+ console.error('[magicbell:install] Installation error:', err);
147
+ }
148
+ finish(reject, err);
149
+ };
150
+ const handleSuccess = () => finish(resolve);
151
+
152
+ debug('Fetching binary', { url: options.url, archiveType: options.archiveType });
153
+ fetch(options.url)
154
+ .then((res) => {
155
+ if (!res.ok) {
156
+ throw new Error(`Error downloading binary from: '${options.url}'. HTTP Status Code: ${res.status}`);
157
+ }
191
158
 
192
- function uninstall(callback) {
193
- const options = parsePackageJson();
159
+ const streams = [];
160
+ if (options.archiveType === 'zip') {
161
+ debug('Using zip extractor');
162
+ streams.push(unzipper.Extract({ path: options.binPath }));
163
+ } else {
164
+ debug('Using gzip+tar extractor');
165
+ streams.push(zlib.createGunzip(), tar.x({ cwd: options.binPath }));
166
+ }
194
167
 
195
- getInstallationPath(function (err, installationPath) {
196
- if (err) {
197
- throw new Error('Error finding binary installation directory');
198
- }
168
+ if (!res.body) {
169
+ throw new Error('Download response did not include a body.');
170
+ }
199
171
 
200
- fs.unlinkSync(path.join(installationPath, options.binName));
201
- callback();
172
+ const bodyStream = typeof res.body.getReader === 'function' ? Readable.fromWeb(res.body) : res.body;
173
+ pipeline(bodyStream, ...streams, (err) => {
174
+ if (err) {
175
+ handleError(err);
176
+ return;
177
+ }
178
+
179
+ try {
180
+ debug('Verifying downloaded binary');
181
+ verifyAndPlaceBinary(options.binName, options.binPath);
182
+ debug('Binary verification successful');
183
+ handleSuccess();
184
+ } catch (verificationError) {
185
+ handleError(verificationError);
186
+ }
187
+ });
188
+ })
189
+ .catch(handleError);
202
190
  });
203
191
  }
204
-
205
- const actions = {
206
- install: install,
207
- uninstall: uninstall,
208
- };
209
-
210
- const argv = process.argv;
211
- if (argv && argv.length > 2) {
212
- const cmd = process.argv[2];
213
- if (!actions[cmd]) {
214
- console.log('Invalid command to go-npm. `install` and `uninstall` are the only supported commands');
215
- process.exit(1);
216
- }
217
-
218
- try {
219
- actions[cmd](() => {
220
- process.exit(0);
221
- });
222
- } catch (err) {
223
- console.error(err);
224
- process.exit(1);
225
- }
226
- }
package/src/log.js ADDED
@@ -0,0 +1,11 @@
1
+ const DEBUG_ENABLED = Boolean(process.env.DEBUG);
2
+
3
+ export function createDebugLogger(scope) {
4
+ return (...args) => {
5
+ if (!DEBUG_ENABLED) return;
6
+ // write to stderr, so it doesn't litter stdout
7
+ console.error(`[magicbell:${scope}]`, args);
8
+ };
9
+ }
10
+
11
+ export const isDebugEnabled = DEBUG_ENABLED;