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.
- package/package.json +44 -0
- 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;
|