magicbell-cli 1.3.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,11 @@
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
+
3
9
  ## 1.3.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "magicbell-cli",
3
- "version": "1.3.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
11
  "magicbell": "src/cli.js"
@@ -22,6 +22,6 @@
22
22
  "goBinary": {
23
23
  "name": "magicbell",
24
24
  "path": "./bin",
25
- "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}}"
26
26
  }
27
27
  }
package/src/cli.js CHANGED
@@ -5,32 +5,76 @@ import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
 
7
7
  import { installBinary } from './install.js';
8
+ import { createDebugLogger } from './log.js';
8
9
 
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
  const BIN_DIR = path.resolve(__dirname, '../bin');
11
12
  const BINARY_NAME = process.platform === 'win32' ? 'magicbell.exe' : 'magicbell';
12
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');
13
17
 
14
18
  async function ensureBinary() {
15
- if (fs.existsSync(BINARY_PATH)) return;
19
+ if (fs.existsSync(BINARY_PATH)) {
20
+ debug('Binary already present at', BINARY_PATH);
21
+ return;
22
+ }
23
+
24
+ debug('Binary missing, installing');
16
25
  await installBinary();
26
+ debug('Installation finished');
17
27
 
18
28
  if (!fs.existsSync(BINARY_PATH)) {
19
29
  throw new Error(`MagicBell binary is missing at ${BINARY_PATH} even after installation.`);
20
30
  }
21
31
  }
22
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
+
23
61
  async function run() {
24
62
  try {
25
63
  await ensureBinary();
64
+ debug('Binary ready, invoking CLI', { args: process.argv.slice(2) });
26
65
  } catch (err) {
27
66
  console.error(err instanceof Error ? err.message : err);
28
67
  process.exit(1);
29
68
  }
30
69
 
31
- const child = spawn(BINARY_PATH, process.argv.slice(2), {
32
- stdio: 'inherit',
33
- });
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
+ }
34
78
 
35
79
  child.on('error', (error) => {
36
80
  console.error('Failed to start the MagicBell binary:', error.message);
@@ -38,6 +82,7 @@ async function run() {
38
82
  });
39
83
 
40
84
  child.on('exit', (code, signal) => {
85
+ debug('Child exited', { code, signal });
41
86
  if (signal) {
42
87
  process.kill(process.pid, signal);
43
88
  return;
package/src/install.js CHANGED
@@ -3,15 +3,19 @@
3
3
  // and used via a lazy-load mechanism instead of during postinstall
4
4
 
5
5
  import fs from 'fs';
6
- import fetch from 'node-fetch';
7
6
  import path from 'path';
7
+ import { pipeline, Readable } from 'stream';
8
8
  import * as tar from 'tar';
9
+ import unzipper from 'unzipper';
9
10
  import { fileURLToPath } from 'url';
10
11
  import zlib from 'zlib';
11
12
 
13
+ import { createDebugLogger, isDebugEnabled } from './log.js';
14
+
12
15
  const __filename = fileURLToPath(import.meta.url);
13
16
  const __dirname = path.dirname(__filename);
14
17
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
18
+ const debug = createDebugLogger('install');
15
19
 
16
20
  // Mapping from Node's `process.arch` to Golang's `$GOARCH`
17
21
  const ARCH_MAPPING = {
@@ -66,6 +70,8 @@ function validateConfiguration(packageJson) {
66
70
  }
67
71
 
68
72
  function loadConfiguration() {
73
+ debug('Loading configuration', { platform: process.platform, arch: process.arch });
74
+ const isWindows = process.platform === 'win32';
69
75
  if (!(process.arch in ARCH_MAPPING)) {
70
76
  throw new Error('Installation is not supported for this architecture: ' + process.arch);
71
77
  }
@@ -91,7 +97,7 @@ function loadConfiguration() {
91
97
  let version = packageJson.goBinary.binaryVersion || packageJson.version;
92
98
  if (version[0] === 'v') version = version.substr(1);
93
99
 
94
- if (process.platform === 'win32') {
100
+ if (isWindows) {
95
101
  binName += '.exe';
96
102
  }
97
103
 
@@ -100,12 +106,16 @@ function loadConfiguration() {
100
106
  url = url.replace(/{{platform}}/g, PLATFORM_MAPPING[process.platform]);
101
107
  url = url.replace(/{{version}}/g, version);
102
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);
103
112
 
104
113
  return {
105
114
  binName,
106
115
  binPath,
107
116
  url,
108
117
  version,
118
+ archiveType: isWindows ? 'zip' : 'tar',
109
119
  };
110
120
  }
111
121
 
@@ -121,8 +131,7 @@ export function installBinary() {
121
131
  const options = loadConfiguration();
122
132
 
123
133
  fs.mkdirSync(options.binPath, { recursive: true });
124
- const ungz = zlib.createGunzip();
125
- const untar = tar.x({ cwd: options.binPath });
134
+ debug('Prepared bin path', options.binPath);
126
135
 
127
136
  return new Promise((resolve, reject) => {
128
137
  let settled = false;
@@ -132,31 +141,50 @@ export function installBinary() {
132
141
  action(value);
133
142
  };
134
143
 
135
- const handleError = (err) => finish(reject, err);
136
- const handleSuccess = () => finish(resolve);
137
-
138
- // First we will Un-GZip, then we will untar. So once untar is completed,
139
- // binary is downloaded into `binPath`. Verify the binary and call it good
140
- untar.on('end', () => {
141
- try {
142
- verifyAndPlaceBinary(options.binName, options.binPath);
143
- handleSuccess();
144
- } catch (err) {
145
- handleError(err);
144
+ const handleError = (err) => {
145
+ if (isDebugEnabled) {
146
+ console.error('[magicbell:install] Installation error:', err);
146
147
  }
147
- });
148
-
149
- ungz.on('error', handleError);
150
- untar.on('error', handleError);
148
+ finish(reject, err);
149
+ };
150
+ const handleSuccess = () => finish(resolve);
151
151
 
152
+ debug('Fetching binary', { url: options.url, archiveType: options.archiveType });
152
153
  fetch(options.url)
153
154
  .then((res) => {
154
155
  if (!res.ok) {
155
- throw new Error('Error downloading binary. HTTP Status Code: ' + res.status);
156
+ throw new Error(`Error downloading binary from: '${options.url}'. HTTP Status Code: ${res.status}`);
157
+ }
158
+
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
+ }
167
+
168
+ if (!res.body) {
169
+ throw new Error('Download response did not include a body.');
156
170
  }
157
171
 
158
- res.body.on('error', handleError);
159
- res.body.pipe(ungz).pipe(untar);
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
+ });
160
188
  })
161
189
  .catch(handleError);
162
190
  });
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;