redblue-cli 0.1.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.
Files changed (2) hide show
  1. package/package.json +44 -0
  2. package/redblue-sdk.js +655 -0
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "redblue-cli",
3
+ "version": "0.1.0",
4
+ "description": "JavaScript SDK wrapper for the redblue CLI",
5
+ "type": "commonjs",
6
+ "main": "./redblue-sdk.js",
7
+ "exports": {
8
+ ".": "./redblue-sdk.js"
9
+ },
10
+ "files": [
11
+ "redblue-sdk.js"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/forattini-dev/redblue.git",
22
+ "directory": "sdk"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/forattini-dev/redblue/issues"
26
+ },
27
+ "homepage": "https://github.com/forattini-dev/redblue",
28
+ "keywords": [
29
+ "redblue",
30
+ "security",
31
+ "cli",
32
+ "sdk",
33
+ "dns",
34
+ "tls",
35
+ "ports"
36
+ ],
37
+ "license": "MIT",
38
+ "scripts": {
39
+ "build": "node --check redblue-sdk.js",
40
+ "test": "node --test --test-reporter=spec test/*.test.js",
41
+ "coverage": "node --test --experimental-test-coverage --test-reporter=spec --test-coverage-include=redblue-sdk.js --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=100 test/*.test.js",
42
+ "pack-check": "npm pack --dry-run"
43
+ }
44
+ }
package/redblue-sdk.js ADDED
@@ -0,0 +1,655 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const fsp = fs.promises;
6
+ const https = require('https');
7
+ const os = require('os');
8
+ const path = require('path');
9
+ const { execFile, spawn } = require('child_process');
10
+
11
+ const DEFAULT_REPO = 'forattini-dev/redblue';
12
+
13
+ function getDefaultBinaryName(platform = process.platform) {
14
+ return platform === 'win32' ? 'rb.exe' : 'rb';
15
+ }
16
+
17
+ const DEFAULT_BINARY_NAME = getDefaultBinaryName();
18
+
19
+ function kebabToCamel(value) {
20
+ return String(value).replace(/[-_]+([a-zA-Z0-9])/g, (_, ch) => ch.toUpperCase());
21
+ }
22
+
23
+ function ensureObject(value, label) {
24
+ if (value == null) {
25
+ return {};
26
+ }
27
+ if (typeof value !== 'object' || Array.isArray(value)) {
28
+ throw new TypeError(`${label} must be an object`);
29
+ }
30
+ return value;
31
+ }
32
+
33
+ function exists(filePath) {
34
+ try {
35
+ fs.accessSync(filePath, fs.constants.F_OK);
36
+ return true;
37
+ } catch (_) {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ function isExecutable(filePath) {
43
+ try {
44
+ fs.accessSync(filePath, fs.constants.X_OK);
45
+ return true;
46
+ } catch (_) {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ function resolveFromPath(binaryName) {
52
+ const pathValue = process.env.PATH || '';
53
+ for (const directory of pathValue.split(path.delimiter)) {
54
+ if (!directory) {
55
+ continue;
56
+ }
57
+ const candidate = path.join(directory, binaryName);
58
+ if (exists(candidate) && (process.platform === 'win32' || isExecutable(candidate))) {
59
+ return candidate;
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+
65
+ function defaultInstallDir() {
66
+ return path.join(os.homedir(), '.redblue', 'bin');
67
+ }
68
+
69
+ function resolveAssetName(options = {}) {
70
+ const platform = options.platform || process.platform;
71
+ const arch = options.arch || process.arch;
72
+ const staticBuild = options.staticBuild === true;
73
+
74
+ if (platform === 'linux' && arch === 'x64') {
75
+ return 'rb-linux-x86_64';
76
+ }
77
+ if (platform === 'linux' && arch === 'arm64') {
78
+ return staticBuild ? 'rb-linux-aarch64-static' : 'rb-linux-aarch64';
79
+ }
80
+ if (platform === 'linux' && (arch === 'arm' || arch === 'armv7l')) {
81
+ return 'rb-linux-armv7';
82
+ }
83
+ if (platform === 'darwin' && arch === 'x64') {
84
+ return 'rb-macos-x86_64';
85
+ }
86
+ if (platform === 'darwin' && arch === 'arm64') {
87
+ return 'rb-macos-aarch64';
88
+ }
89
+ if (platform === 'win32' && arch === 'x64') {
90
+ return 'rb-windows-x86_64.exe';
91
+ }
92
+
93
+ throw new Error(`Unsupported redblue platform combination: ${platform}/${arch}`);
94
+ }
95
+
96
+ function request(url, options = {}) {
97
+ return new Promise((resolve, reject) => {
98
+ const headers = Object.assign(
99
+ {
100
+ 'User-Agent': 'redblue-sdk',
101
+ Accept: 'application/vnd.github+json'
102
+ },
103
+ options.headers || {}
104
+ );
105
+
106
+ const req = https.request(url, { headers }, (res) => {
107
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
108
+ res.resume();
109
+ resolve(request(res.headers.location, options));
110
+ return;
111
+ }
112
+
113
+ const chunks = [];
114
+ res.on('data', (chunk) => chunks.push(chunk));
115
+ res.on('end', () => {
116
+ const body = Buffer.concat(chunks);
117
+ if (res.statusCode < 200 || res.statusCode >= 300) {
118
+ const error = new Error(
119
+ `Request failed with status ${res.statusCode}: ${body.toString('utf8')}`
120
+ );
121
+ error.statusCode = res.statusCode;
122
+ error.body = body.toString('utf8');
123
+ reject(error);
124
+ return;
125
+ }
126
+ resolve({ res, body });
127
+ });
128
+ });
129
+
130
+ req.on('error', reject);
131
+ req.end();
132
+ });
133
+ }
134
+
135
+ async function requestJson(url, options = {}) {
136
+ const { body } = await request(url, options);
137
+ return JSON.parse(body.toString('utf8'));
138
+ }
139
+
140
+ async function requestText(url, options = {}) {
141
+ const { body } = await request(url, options);
142
+ return body.toString('utf8');
143
+ }
144
+
145
+ async function getReleaseTag(options = {}) {
146
+ const repo = options.repo || DEFAULT_REPO;
147
+ const githubToken = options.githubToken || process.env.GITHUB_TOKEN;
148
+ const headers = githubToken ? { Authorization: `Bearer ${githubToken}` } : {};
149
+
150
+ if (options.version) {
151
+ return String(options.version).startsWith('v')
152
+ ? String(options.version)
153
+ : `v${options.version}`;
154
+ }
155
+
156
+ const channel = options.channel || 'stable';
157
+
158
+ if (channel === 'stable') {
159
+ const release = await requestJson(`https://api.github.com/repos/${repo}/releases/latest`, {
160
+ headers
161
+ });
162
+ return release.tag_name;
163
+ }
164
+
165
+ const releases = await requestJson(`https://api.github.com/repos/${repo}/releases`, {
166
+ headers
167
+ });
168
+
169
+ if (!Array.isArray(releases) || releases.length === 0) {
170
+ throw new Error(`No releases found for ${repo}`);
171
+ }
172
+
173
+ if (channel === 'next') {
174
+ const prerelease = releases.find((release) => release && release.prerelease);
175
+ if (prerelease) {
176
+ return prerelease.tag_name;
177
+ }
178
+ return releases[0].tag_name;
179
+ }
180
+
181
+ if (channel === 'latest') {
182
+ return releases[0].tag_name;
183
+ }
184
+
185
+ throw new Error(`Unsupported release channel: ${channel}`);
186
+ }
187
+
188
+ async function downloadToFile(url, destination, options = {}) {
189
+ const { body } = await request(url, options);
190
+ await fsp.mkdir(path.dirname(destination), { recursive: true });
191
+ await fsp.writeFile(destination, body);
192
+ }
193
+
194
+ async function sha256File(filePath) {
195
+ const hash = crypto.createHash('sha256');
196
+ const file = await fsp.readFile(filePath);
197
+ hash.update(file);
198
+ return hash.digest('hex');
199
+ }
200
+
201
+ async function verifyChecksum(filePath, checksumUrl, options = {}) {
202
+ try {
203
+ const checksumText = await requestText(checksumUrl, options);
204
+ const expected = checksumText.trim().split(/\s+/)[0];
205
+ if (!expected) {
206
+ return;
207
+ }
208
+ const actual = await sha256File(filePath);
209
+ if (expected !== actual) {
210
+ throw new Error(
211
+ `Checksum mismatch for ${path.basename(filePath)}: expected ${expected}, got ${actual}`
212
+ );
213
+ }
214
+ } catch (error) {
215
+ if (error && error.statusCode === 404) {
216
+ return;
217
+ }
218
+ throw error;
219
+ }
220
+ }
221
+
222
+ async function downloadBinary(options = {}) {
223
+ const repo = options.repo || DEFAULT_REPO;
224
+ const assetName = options.assetName || resolveAssetName(options);
225
+ const installDir = options.targetDir || defaultInstallDir();
226
+ const binaryName = options.binaryName || DEFAULT_BINARY_NAME;
227
+ const destination = path.resolve(installDir, binaryName);
228
+ const releaseTag = await getReleaseTag(options);
229
+ const githubToken = options.githubToken || process.env.GITHUB_TOKEN;
230
+ const headers = githubToken ? { Authorization: `Bearer ${githubToken}` } : {};
231
+ const assetUrl = `https://github.com/${repo}/releases/download/${releaseTag}/${assetName}`;
232
+ const checksumUrl = `${assetUrl}.sha256`;
233
+
234
+ await downloadToFile(assetUrl, destination, { headers });
235
+
236
+ if (process.platform !== 'win32') {
237
+ await fsp.chmod(destination, 0o755);
238
+ }
239
+
240
+ if (options.verify !== false) {
241
+ await verifyChecksum(destination, checksumUrl, { headers });
242
+ }
243
+
244
+ return destination;
245
+ }
246
+
247
+ async function resolveBinary(options = {}) {
248
+ if (options.binaryPath) {
249
+ const binaryPath = path.resolve(options.binaryPath);
250
+ if (!exists(binaryPath)) {
251
+ throw new Error(`redblue binary not found at ${binaryPath}`);
252
+ }
253
+ return binaryPath;
254
+ }
255
+
256
+ const installDir = options.targetDir || defaultInstallDir();
257
+ const binaryName = options.binaryName || DEFAULT_BINARY_NAME;
258
+ const installedCandidate = path.resolve(installDir, binaryName);
259
+ if (exists(installedCandidate)) {
260
+ return installedCandidate;
261
+ }
262
+
263
+ const pathCandidate = resolveFromPath(binaryName);
264
+ if (pathCandidate) {
265
+ return pathCandidate;
266
+ }
267
+
268
+ if (options.autoDownload) {
269
+ return downloadBinary(options);
270
+ }
271
+
272
+ throw new Error(
273
+ `Unable to resolve redblue binary. Set binaryPath, provide autoDownload=true, or install ${binaryName} in PATH.`
274
+ );
275
+ }
276
+
277
+ function execFilePromise(binaryPath, args, options = {}) {
278
+ return new Promise((resolve, reject) => {
279
+ execFile(
280
+ binaryPath,
281
+ args,
282
+ {
283
+ cwd: options.cwd,
284
+ env: options.env,
285
+ timeout: options.timeout,
286
+ maxBuffer: options.maxBuffer || 32 * 1024 * 1024
287
+ },
288
+ (error, stdout, stderr) => {
289
+ if (error) {
290
+ error.stdout = stdout;
291
+ error.stderr = stderr;
292
+ reject(error);
293
+ return;
294
+ }
295
+
296
+ resolve({
297
+ code: 0,
298
+ stdout,
299
+ stderr,
300
+ args: [binaryPath].concat(args)
301
+ });
302
+ }
303
+ );
304
+ });
305
+ }
306
+
307
+ function spawnBinary(binaryPath, args, options = {}) {
308
+ return spawn(binaryPath, args, {
309
+ cwd: options.cwd,
310
+ env: options.env,
311
+ stdio: options.stdio || 'inherit',
312
+ detached: options.detached === true
313
+ });
314
+ }
315
+
316
+ async function getManifest(options = {}) {
317
+ const binaryPath = await resolveBinary(options);
318
+ const result = await execFilePromise(binaryPath, ['sdk', 'bridge', 'manifest'], options);
319
+ const stdout = String(result.stdout || '').trim();
320
+
321
+ if (!stdout) {
322
+ throw new Error('redblue SDK manifest command returned empty output');
323
+ }
324
+
325
+ try {
326
+ return {
327
+ binaryPath,
328
+ manifest: JSON.parse(stdout)
329
+ };
330
+ } catch (error) {
331
+ const wrapped = new Error(`Failed to parse redblue SDK manifest JSON: ${error.message}`);
332
+ wrapped.stdout = stdout;
333
+ throw wrapped;
334
+ }
335
+ }
336
+
337
+ function findFlag(command, key) {
338
+ return (command.flags || []).find(
339
+ (flag) => flag.long === key || flag.camel_name === key || flag.short === key
340
+ );
341
+ }
342
+
343
+ function buildInvocation(command, route, input, execOptions) {
344
+ const payload = ensureObject(input, 'route input');
345
+ const args = [command.domain, command.resource, route.verb];
346
+ const consumedKeys = new Set();
347
+ const positionals = Array.isArray(route.positionals) ? route.positionals : [];
348
+ const flags = Array.isArray(command.flags) ? command.flags : [];
349
+ const extraFlags = ensureObject(payload.flags, 'flags');
350
+ const preferredFlag =
351
+ command.machine_output && typeof command.machine_output.preferred_flag === 'string'
352
+ ? command.machine_output.preferred_flag
353
+ : null;
354
+ const preferredValue =
355
+ command.machine_output && typeof command.machine_output.preferred_value === 'string'
356
+ ? command.machine_output.preferred_value
357
+ : 'json';
358
+
359
+ for (const positional of positionals) {
360
+ let value;
361
+ if (positional.slot === 'target') {
362
+ value = payload[positional.name];
363
+ if (value === undefined && positional.name !== 'target') {
364
+ value = payload.target;
365
+ }
366
+ consumedKeys.add(positional.name);
367
+ if (payload.target !== undefined) {
368
+ consumedKeys.add('target');
369
+ }
370
+ } else {
371
+ value = payload[positional.name];
372
+ consumedKeys.add(positional.name);
373
+ }
374
+
375
+ if (value === undefined || value === null || value === '') {
376
+ if (positional.required) {
377
+ throw new Error(
378
+ `Missing required positional "${positional.name}" for ${command.domain} ${command.resource} ${route.verb}`
379
+ );
380
+ }
381
+ continue;
382
+ }
383
+
384
+ if (positional.repeated) {
385
+ const values = Array.isArray(value) ? value : [value];
386
+ for (const item of values) {
387
+ args.push(String(item));
388
+ }
389
+ continue;
390
+ }
391
+
392
+ args.push(String(value));
393
+ }
394
+
395
+ if (positionals.length === 0 && payload.target !== undefined && payload.target !== null) {
396
+ args.push(String(payload.target));
397
+ consumedKeys.add('target');
398
+ }
399
+
400
+ const extraArgs = payload.args;
401
+ if (extraArgs !== undefined) {
402
+ if (!Array.isArray(extraArgs)) {
403
+ throw new TypeError('args must be an array when provided');
404
+ }
405
+ for (const item of extraArgs) {
406
+ args.push(String(item));
407
+ }
408
+ consumedKeys.add('args');
409
+ }
410
+
411
+ consumedKeys.add('flags');
412
+
413
+ const explicitMachineFlag = new Set();
414
+
415
+ for (const flag of flags) {
416
+ const longName = flag.long;
417
+ const camelName = flag.camel_name || kebabToCamel(longName);
418
+ let value;
419
+ let usedKey = null;
420
+
421
+ if (Object.prototype.hasOwnProperty.call(payload, longName)) {
422
+ value = payload[longName];
423
+ usedKey = longName;
424
+ } else if (Object.prototype.hasOwnProperty.call(payload, camelName)) {
425
+ value = payload[camelName];
426
+ usedKey = camelName;
427
+ } else if (flag.short && Object.prototype.hasOwnProperty.call(payload, flag.short)) {
428
+ value = payload[flag.short];
429
+ usedKey = flag.short;
430
+ } else if (Object.prototype.hasOwnProperty.call(extraFlags, longName)) {
431
+ value = extraFlags[longName];
432
+ usedKey = `flags.${longName}`;
433
+ } else if (Object.prototype.hasOwnProperty.call(extraFlags, camelName)) {
434
+ value = extraFlags[camelName];
435
+ usedKey = `flags.${camelName}`;
436
+ }
437
+
438
+ if (usedKey) {
439
+ consumedKeys.add(usedKey.split('.')[0]);
440
+ explicitMachineFlag.add(longName);
441
+ }
442
+
443
+ if (value === undefined || value === null || value === false) {
444
+ continue;
445
+ }
446
+
447
+ if (flag.expects_value) {
448
+ if (Array.isArray(value)) {
449
+ for (const item of value) {
450
+ args.push(`--${longName}`, String(item));
451
+ }
452
+ } else {
453
+ args.push(`--${longName}`, String(value));
454
+ }
455
+ } else {
456
+ args.push(`--${longName}`);
457
+ }
458
+ }
459
+
460
+ for (const key of Object.keys(extraFlags)) {
461
+ if (!findFlag(command, key)) {
462
+ throw new Error(
463
+ `Unknown flag "${key}" for ${command.domain} ${command.resource} ${route.verb}`
464
+ );
465
+ }
466
+ }
467
+
468
+ const knownKeys = new Set([
469
+ 'target',
470
+ 'args',
471
+ 'flags',
472
+ 'cwd',
473
+ 'env',
474
+ 'timeout',
475
+ 'maxBuffer',
476
+ 'stdio'
477
+ ]);
478
+
479
+ for (const key of Object.keys(payload)) {
480
+ if (!consumedKeys.has(key) && !knownKeys.has(key) && !findFlag(command, key)) {
481
+ throw new Error(
482
+ `Unknown parameter "${key}" for ${command.domain} ${command.resource} ${route.verb}`
483
+ );
484
+ }
485
+ }
486
+
487
+ const wantsJson = execOptions.json !== false;
488
+ if (wantsJson) {
489
+ args.push('--json');
490
+ if (preferredFlag && !explicitMachineFlag.has(preferredFlag)) {
491
+ args.push(`--${preferredFlag}`, preferredValue);
492
+ }
493
+ }
494
+
495
+ return args;
496
+ }
497
+
498
+ async function invokeJson(binaryPath, command, route, input, execOptions = {}, defaults = {}) {
499
+ const args = buildInvocation(command, route, input, execOptions);
500
+ const result = await execFilePromise(binaryPath, args, {
501
+ cwd: execOptions.cwd || defaults.cwd,
502
+ env: Object.assign({}, defaults.env || {}, execOptions.env || {}),
503
+ timeout: execOptions.timeout || defaults.timeout,
504
+ maxBuffer: execOptions.maxBuffer || defaults.maxBuffer
505
+ });
506
+ const stdout = String(result.stdout || '').trim();
507
+
508
+ if (!stdout) {
509
+ return null;
510
+ }
511
+
512
+ try {
513
+ return JSON.parse(stdout);
514
+ } catch (error) {
515
+ const wrapped = new Error(
516
+ `redblue command did not emit valid JSON for ${command.domain} ${command.resource} ${route.verb}: ${error.message}`
517
+ );
518
+ wrapped.stdout = stdout;
519
+ wrapped.stderr = result.stderr;
520
+ wrapped.args = args;
521
+ throw wrapped;
522
+ }
523
+ }
524
+
525
+ async function invokeRaw(binaryPath, command, route, input, execOptions = {}, defaults = {}) {
526
+ const args = buildInvocation(command, route, input, Object.assign({}, execOptions, { json: false }));
527
+ return execFilePromise(binaryPath, args, {
528
+ cwd: execOptions.cwd || defaults.cwd,
529
+ env: Object.assign({}, defaults.env || {}, execOptions.env || {}),
530
+ timeout: execOptions.timeout || defaults.timeout,
531
+ maxBuffer: execOptions.maxBuffer || defaults.maxBuffer
532
+ });
533
+ }
534
+
535
+ function attachRoute(container, binaryPath, command, route, defaults) {
536
+ const invoke = async function invoke(input = {}, execOptions = {}) {
537
+ return invokeJson(binaryPath, command, route, input, execOptions, defaults);
538
+ };
539
+
540
+ invoke.raw = function raw(input = {}, execOptions = {}) {
541
+ return invokeRaw(binaryPath, command, route, input, execOptions, defaults);
542
+ };
543
+
544
+ invoke.spawn = function spawnRoute(input = {}, spawnOptions = {}) {
545
+ const args = buildInvocation(command, route, input, Object.assign({}, spawnOptions, { json: false }));
546
+ return spawnBinary(binaryPath, args, {
547
+ cwd: spawnOptions.cwd || defaults.cwd,
548
+ env: Object.assign({}, defaults.env || {}, spawnOptions.env || {}),
549
+ stdio: spawnOptions.stdio,
550
+ detached: spawnOptions.detached
551
+ });
552
+ };
553
+
554
+ invoke.meta = { command, route };
555
+ container[route.verb] = invoke;
556
+ }
557
+
558
+ function createDomainProxy(binaryPath, manifest, defaults) {
559
+ const client = {};
560
+
561
+ for (const command of manifest.commands || []) {
562
+ if (!client[command.domain]) {
563
+ client[command.domain] = {};
564
+ }
565
+ if (!client[command.domain][command.resource]) {
566
+ client[command.domain][command.resource] = {};
567
+ }
568
+
569
+ for (const route of command.routes || []) {
570
+ attachRoute(client[command.domain][command.resource], binaryPath, command, route, defaults);
571
+ }
572
+ }
573
+
574
+ return client;
575
+ }
576
+
577
+ async function createClient(options = {}) {
578
+ const defaults = ensureObject(options, 'createClient options');
579
+ const { binaryPath, manifest } = await getManifest(defaults);
580
+ const api = createDomainProxy(binaryPath, manifest, defaults);
581
+
582
+ Object.defineProperties(api, {
583
+ $binaryPath: {
584
+ value: binaryPath,
585
+ enumerable: false
586
+ },
587
+ $manifest: {
588
+ value: manifest,
589
+ enumerable: false
590
+ },
591
+ $downloadBinary: {
592
+ value: downloadBinary,
593
+ enumerable: false
594
+ },
595
+ $resolveBinary: {
596
+ value: resolveBinary,
597
+ enumerable: false
598
+ },
599
+ $exec: {
600
+ value(args, execOptions = {}) {
601
+ if (!Array.isArray(args)) {
602
+ throw new TypeError('$exec expects an array of CLI arguments');
603
+ }
604
+ return execFilePromise(binaryPath, args, Object.assign({}, defaults, execOptions));
605
+ },
606
+ enumerable: false
607
+ },
608
+ $spawn: {
609
+ value(args, spawnOptions = {}) {
610
+ if (!Array.isArray(args)) {
611
+ throw new TypeError('$spawn expects an array of CLI arguments');
612
+ }
613
+ return spawnBinary(binaryPath, args, Object.assign({}, defaults, spawnOptions));
614
+ },
615
+ enumerable: false
616
+ }
617
+ });
618
+
619
+ return api;
620
+ }
621
+
622
+ module.exports = {
623
+ createClient,
624
+ downloadBinary,
625
+ getManifest,
626
+ resolveAssetName,
627
+ resolveBinary
628
+ };
629
+
630
+ module.exports._internal = {
631
+ attachRoute,
632
+ buildInvocation,
633
+ createDomainProxy,
634
+ defaultInstallDir,
635
+ downloadToFile,
636
+ ensureObject,
637
+ execFilePromise,
638
+ exists,
639
+ findFlag,
640
+ getDefaultBinaryName,
641
+ getReleaseTag,
642
+ invokeJson,
643
+ invokeRaw,
644
+ isExecutable,
645
+ kebabToCamel,
646
+ request,
647
+ requestJson,
648
+ requestText,
649
+ resolveFromPath,
650
+ sha256File,
651
+ spawnBinary,
652
+ verifyChecksum
653
+ };
654
+
655
+ module.exports.default = module.exports;