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 +6 -0
- package/package.json +4 -4
- package/src/cli.js +49 -4
- package/src/install.js +50 -22
- package/src/log.js +11 -0
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
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "MagicBell CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
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}}
|
|
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))
|
|
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
|
-
|
|
32
|
-
|
|
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 (
|
|
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
|
-
|
|
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) =>
|
|
136
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
159
|
-
|
|
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;
|