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 +12 -0
- package/package.json +5 -9
- package/src/cli.js +95 -0
- package/src/install.js +88 -123
- package/src/log.js +11 -0
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.
|
|
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
|
-
"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}}
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
return;
|
|
80
|
+
throw new Error('Installation is not supported for this platform: ' + process.platform);
|
|
115
81
|
}
|
|
116
82
|
|
|
117
|
-
const packageJsonPath = path.join(
|
|
83
|
+
const packageJsonPath = path.join(PACKAGE_ROOT, 'package.json');
|
|
118
84
|
if (!fs.existsSync(packageJsonPath)) {
|
|
119
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
150
|
-
binPath
|
|
151
|
-
url
|
|
152
|
-
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
168
|
+
if (!res.body) {
|
|
169
|
+
throw new Error('Download response did not include a body.');
|
|
170
|
+
}
|
|
199
171
|
|
|
200
|
-
|
|
201
|
-
|
|
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;
|