redblue-cli 0.1.0-next.48

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.
@@ -0,0 +1,949 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const crypto = require('crypto');
5
+ const fs = require('fs');
6
+ const fsp = fs.promises;
7
+ const https = require('https');
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const { pathToFileURL } = require('url');
11
+ const { execFile, spawn } = require('child_process');
12
+
13
+ const DEFAULT_REPO = 'forattini-dev/redblue';
14
+ const LOCAL_CLI_ARGS_PARSER_PATH = path.resolve(
15
+ __dirname,
16
+ '../../../tetis/libs/cli-args-parser/dist/index.js'
17
+ );
18
+ const WRAPPER_OPTION_TYPES = Object.freeze({
19
+ 'asset-name': 'string',
20
+ 'auto-download': 'boolean',
21
+ 'binary-path': 'string',
22
+ channel: 'string',
23
+ download: 'boolean',
24
+ 'github-token': 'string',
25
+ 'no-verify': 'boolean',
26
+ repo: 'string',
27
+ 'sdk-help': 'boolean',
28
+ 'static-build': 'boolean',
29
+ 'target-dir': 'string',
30
+ version: 'string',
31
+ verify: 'boolean'
32
+ });
33
+ const WRAPPER_OPTION_SCHEMA = Object.freeze({
34
+ options: {
35
+ 'asset-name': {
36
+ type: 'string'
37
+ },
38
+ 'auto-download': {
39
+ type: 'boolean',
40
+ aliases: ['download']
41
+ },
42
+ 'binary-path': {
43
+ type: 'string'
44
+ },
45
+ channel: {
46
+ type: 'string'
47
+ },
48
+ 'github-token': {
49
+ type: 'string'
50
+ },
51
+ repo: {
52
+ type: 'string'
53
+ },
54
+ 'sdk-help': {
55
+ type: 'boolean'
56
+ },
57
+ 'static-build': {
58
+ type: 'boolean'
59
+ },
60
+ 'target-dir': {
61
+ type: 'string'
62
+ },
63
+ version: {
64
+ type: 'string'
65
+ },
66
+ verify: {
67
+ type: 'boolean'
68
+ }
69
+ }
70
+ });
71
+
72
+ function getDefaultBinaryName(platform = process.platform) {
73
+ return platform === 'win32' ? 'rb.exe' : 'rb';
74
+ }
75
+
76
+ const DEFAULT_BINARY_NAME = getDefaultBinaryName();
77
+
78
+ function kebabToCamel(value) {
79
+ return String(value).replace(/[-_]+([a-zA-Z0-9])/g, (_, ch) => ch.toUpperCase());
80
+ }
81
+
82
+ function ensureObject(value, label) {
83
+ if (value == null) {
84
+ return {};
85
+ }
86
+ if (typeof value !== 'object' || Array.isArray(value)) {
87
+ throw new TypeError(`${label} must be an object`);
88
+ }
89
+ return value;
90
+ }
91
+
92
+ function exists(filePath) {
93
+ try {
94
+ fs.accessSync(filePath, fs.constants.F_OK);
95
+ return true;
96
+ } catch (_) {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ function isExecutable(filePath) {
102
+ try {
103
+ fs.accessSync(filePath, fs.constants.X_OK);
104
+ return true;
105
+ } catch (_) {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ function resolveFromPath(binaryName) {
111
+ const pathValue = process.env.PATH || '';
112
+ for (const directory of pathValue.split(path.delimiter)) {
113
+ if (!directory) {
114
+ continue;
115
+ }
116
+ const candidate = path.join(directory, binaryName);
117
+ if (exists(candidate) && (process.platform === 'win32' || isExecutable(candidate))) {
118
+ return candidate;
119
+ }
120
+ }
121
+ return null;
122
+ }
123
+
124
+ function defaultInstallDir() {
125
+ return path.join(os.homedir(), '.redblue', 'bin');
126
+ }
127
+
128
+ function resolveAssetName(options = {}) {
129
+ const platform = options.platform || process.platform;
130
+ const arch = options.arch || process.arch;
131
+ const staticBuild = options.staticBuild === true;
132
+
133
+ if (platform === 'linux' && arch === 'x64') {
134
+ return 'rb-linux-x86_64';
135
+ }
136
+ if (platform === 'linux' && arch === 'arm64') {
137
+ return staticBuild ? 'rb-linux-aarch64-static' : 'rb-linux-aarch64';
138
+ }
139
+ if (platform === 'linux' && (arch === 'arm' || arch === 'armv7l')) {
140
+ return 'rb-linux-armv7';
141
+ }
142
+ if (platform === 'darwin' && arch === 'x64') {
143
+ return 'rb-macos-x86_64';
144
+ }
145
+ if (platform === 'darwin' && arch === 'arm64') {
146
+ return 'rb-macos-aarch64';
147
+ }
148
+ if (platform === 'win32' && arch === 'x64') {
149
+ return 'rb-windows-x86_64.exe';
150
+ }
151
+
152
+ throw new Error(`Unsupported redblue platform combination: ${platform}/${arch}`);
153
+ }
154
+
155
+ function request(url, options = {}) {
156
+ return new Promise((resolve, reject) => {
157
+ const headers = Object.assign(
158
+ {
159
+ 'User-Agent': 'redblue-sdk',
160
+ Accept: 'application/vnd.github+json'
161
+ },
162
+ options.headers || {}
163
+ );
164
+
165
+ const req = https.request(url, { headers }, (res) => {
166
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
167
+ res.resume();
168
+ resolve(request(res.headers.location, options));
169
+ return;
170
+ }
171
+
172
+ const chunks = [];
173
+ res.on('data', (chunk) => chunks.push(chunk));
174
+ res.on('end', () => {
175
+ const body = Buffer.concat(chunks);
176
+ if (res.statusCode < 200 || res.statusCode >= 300) {
177
+ const error = new Error(
178
+ `Request failed with status ${res.statusCode}: ${body.toString('utf8')}`
179
+ );
180
+ error.statusCode = res.statusCode;
181
+ error.body = body.toString('utf8');
182
+ reject(error);
183
+ return;
184
+ }
185
+ resolve({ res, body });
186
+ });
187
+ });
188
+
189
+ req.on('error', reject);
190
+ req.end();
191
+ });
192
+ }
193
+
194
+ async function requestJson(url, options = {}) {
195
+ const { body } = await request(url, options);
196
+ return JSON.parse(body.toString('utf8'));
197
+ }
198
+
199
+ async function requestText(url, options = {}) {
200
+ const { body } = await request(url, options);
201
+ return body.toString('utf8');
202
+ }
203
+
204
+ async function getReleaseTag(options = {}) {
205
+ const repo = options.repo || DEFAULT_REPO;
206
+ const githubToken = options.githubToken || process.env.GITHUB_TOKEN;
207
+ const headers = githubToken ? { Authorization: `Bearer ${githubToken}` } : {};
208
+
209
+ if (options.version) {
210
+ return String(options.version).startsWith('v')
211
+ ? String(options.version)
212
+ : `v${options.version}`;
213
+ }
214
+
215
+ const channel = options.channel || 'stable';
216
+
217
+ if (channel === 'stable') {
218
+ const release = await requestJson(`https://api.github.com/repos/${repo}/releases/latest`, {
219
+ headers
220
+ });
221
+ return release.tag_name;
222
+ }
223
+
224
+ const releases = await requestJson(`https://api.github.com/repos/${repo}/releases`, {
225
+ headers
226
+ });
227
+
228
+ if (!Array.isArray(releases) || releases.length === 0) {
229
+ throw new Error(`No releases found for ${repo}`);
230
+ }
231
+
232
+ if (channel === 'next') {
233
+ const prerelease = releases.find((release) => release && release.prerelease);
234
+ if (prerelease) {
235
+ return prerelease.tag_name;
236
+ }
237
+ return releases[0].tag_name;
238
+ }
239
+
240
+ if (channel === 'latest') {
241
+ return releases[0].tag_name;
242
+ }
243
+
244
+ throw new Error(`Unsupported release channel: ${channel}`);
245
+ }
246
+
247
+ async function downloadToFile(url, destination, options = {}) {
248
+ const { body } = await request(url, options);
249
+ await fsp.mkdir(path.dirname(destination), { recursive: true });
250
+ await fsp.writeFile(destination, body);
251
+ }
252
+
253
+ async function sha256File(filePath) {
254
+ const hash = crypto.createHash('sha256');
255
+ const file = await fsp.readFile(filePath);
256
+ hash.update(file);
257
+ return hash.digest('hex');
258
+ }
259
+
260
+ async function verifyChecksum(filePath, checksumUrl, options = {}) {
261
+ try {
262
+ const checksumText = await requestText(checksumUrl, options);
263
+ const expected = checksumText.trim().split(/\s+/)[0];
264
+ if (!expected) {
265
+ return;
266
+ }
267
+ const actual = await sha256File(filePath);
268
+ if (expected !== actual) {
269
+ throw new Error(
270
+ `Checksum mismatch for ${path.basename(filePath)}: expected ${expected}, got ${actual}`
271
+ );
272
+ }
273
+ } catch (error) {
274
+ if (error && error.statusCode === 404) {
275
+ return;
276
+ }
277
+ throw error;
278
+ }
279
+ }
280
+
281
+ async function downloadBinary(options = {}) {
282
+ const repo = options.repo || DEFAULT_REPO;
283
+ const assetName = options.assetName || resolveAssetName(options);
284
+ const installDir = options.targetDir || defaultInstallDir();
285
+ const binaryName = options.binaryName || DEFAULT_BINARY_NAME;
286
+ const destination = path.resolve(installDir, binaryName);
287
+ const releaseTag = await getReleaseTag(options);
288
+ const githubToken = options.githubToken || process.env.GITHUB_TOKEN;
289
+ const headers = githubToken ? { Authorization: `Bearer ${githubToken}` } : {};
290
+ const assetUrl = `https://github.com/${repo}/releases/download/${releaseTag}/${assetName}`;
291
+ const checksumUrl = `${assetUrl}.sha256`;
292
+
293
+ await downloadToFile(assetUrl, destination, { headers });
294
+
295
+ if (process.platform !== 'win32') {
296
+ await fsp.chmod(destination, 0o755);
297
+ }
298
+
299
+ if (options.verify !== false) {
300
+ await verifyChecksum(destination, checksumUrl, { headers });
301
+ }
302
+
303
+ return destination;
304
+ }
305
+
306
+ async function resolveBinary(options = {}) {
307
+ if (options.binaryPath) {
308
+ const binaryPath = path.resolve(options.binaryPath);
309
+ if (!exists(binaryPath)) {
310
+ throw new Error(`redblue binary not found at ${binaryPath}`);
311
+ }
312
+ return binaryPath;
313
+ }
314
+
315
+ const installDir = options.targetDir || defaultInstallDir();
316
+ const binaryName = options.binaryName || DEFAULT_BINARY_NAME;
317
+ const installedCandidate = path.resolve(installDir, binaryName);
318
+ if (exists(installedCandidate)) {
319
+ return installedCandidate;
320
+ }
321
+
322
+ const pathCandidate = resolveFromPath(binaryName);
323
+ if (pathCandidate) {
324
+ return pathCandidate;
325
+ }
326
+
327
+ if (options.autoDownload) {
328
+ return downloadBinary(options);
329
+ }
330
+
331
+ throw new Error(
332
+ `Unable to resolve redblue binary. Set binaryPath, provide autoDownload=true, or install ${binaryName} in PATH.`
333
+ );
334
+ }
335
+
336
+ function execFilePromise(binaryPath, args, options = {}) {
337
+ return new Promise((resolve, reject) => {
338
+ execFile(
339
+ binaryPath,
340
+ args,
341
+ {
342
+ cwd: options.cwd,
343
+ env: options.env,
344
+ timeout: options.timeout,
345
+ maxBuffer: options.maxBuffer || 32 * 1024 * 1024
346
+ },
347
+ (error, stdout, stderr) => {
348
+ if (error) {
349
+ error.stdout = stdout;
350
+ error.stderr = stderr;
351
+ reject(error);
352
+ return;
353
+ }
354
+
355
+ resolve({
356
+ code: 0,
357
+ stdout,
358
+ stderr,
359
+ args: [binaryPath].concat(args)
360
+ });
361
+ }
362
+ );
363
+ });
364
+ }
365
+
366
+ function spawnBinary(binaryPath, args, options = {}) {
367
+ return spawn(binaryPath, args, {
368
+ cwd: options.cwd,
369
+ env: options.env,
370
+ stdio: options.stdio || 'inherit',
371
+ detached: options.detached === true
372
+ });
373
+ }
374
+
375
+ function toImportSpecifier(filePath) {
376
+ return pathToFileURL(path.resolve(filePath)).href;
377
+ }
378
+
379
+ function getParserCandidatePaths(runtime = {}) {
380
+ const env = runtime.env || process.env;
381
+ const localParserPath = runtime.localParserPath || LOCAL_CLI_ARGS_PARSER_PATH;
382
+ const candidates = [];
383
+ const seen = new Set();
384
+
385
+ function pushCandidate(specifier) {
386
+ if (!specifier || seen.has(specifier)) {
387
+ return;
388
+ }
389
+ seen.add(specifier);
390
+ candidates.push(specifier);
391
+ }
392
+
393
+ if (env.REDBLUE_CLI_ARGS_PARSER_PATH) {
394
+ pushCandidate(toImportSpecifier(env.REDBLUE_CLI_ARGS_PARSER_PATH));
395
+ }
396
+
397
+ pushCandidate('cli-args-parser');
398
+
399
+ if (localParserPath && exists(localParserPath)) {
400
+ pushCandidate(toImportSpecifier(localParserPath));
401
+ }
402
+
403
+ return candidates;
404
+ }
405
+
406
+ async function loadCliArgsParser(runtime = {}) {
407
+ if (runtime.parserModule) {
408
+ return runtime.parserModule;
409
+ }
410
+
411
+ const importModule =
412
+ runtime.importModule ||
413
+ (async function defaultImport(specifier) {
414
+ return import(specifier);
415
+ });
416
+
417
+ const candidates = Array.isArray(runtime.parserCandidates)
418
+ ? runtime.parserCandidates.slice()
419
+ : getParserCandidatePaths(runtime);
420
+ const failures = [];
421
+
422
+ for (const specifier of candidates) {
423
+ try {
424
+ return await importModule(specifier);
425
+ } catch (error) {
426
+ failures.push(`${specifier}: ${error.message}`);
427
+ }
428
+ }
429
+
430
+ /* node:coverage disable */
431
+ const failureSummary = failures.length > 0 ? failures.join('; ') : 'no candidates available';
432
+ /* node:coverage enable */
433
+ throw new Error(`Unable to load cli-args-parser. Tried: ${failureSummary}`);
434
+ }
435
+
436
+ function splitWrapperArgs(argv) {
437
+ const rawArgs = Array.isArray(argv) ? argv.slice() : [];
438
+ const wrapperArgs = [];
439
+ let index = 0;
440
+
441
+ while (index < rawArgs.length) {
442
+ const token = rawArgs[index];
443
+
444
+ if (token === '--') {
445
+ return {
446
+ wrapperArgs,
447
+ passthroughArgs: rawArgs.slice(index + 1),
448
+ usedDoubleDash: true
449
+ };
450
+ }
451
+
452
+ if (!token || !token.startsWith('--')) {
453
+ break;
454
+ }
455
+
456
+ const eqIndex = token.indexOf('=');
457
+ const optionName = token.slice(2, eqIndex === -1 ? undefined : eqIndex);
458
+ const optionType = WRAPPER_OPTION_TYPES[optionName];
459
+
460
+ if (!optionType) {
461
+ break;
462
+ }
463
+
464
+ wrapperArgs.push(token);
465
+ index += 1;
466
+
467
+ if (optionType === 'string' && eqIndex === -1 && index < rawArgs.length) {
468
+ wrapperArgs.push(rawArgs[index]);
469
+ index += 1;
470
+ }
471
+ }
472
+
473
+ return {
474
+ wrapperArgs,
475
+ passthroughArgs: rawArgs.slice(index),
476
+ usedDoubleDash: false
477
+ };
478
+ }
479
+
480
+ async function parseWrapperArgs(argv, runtime = {}) {
481
+ const rawArgs = Array.isArray(argv) ? argv.slice() : [];
482
+ const parserModule = await loadCliArgsParser(runtime);
483
+ const { createParser } = parserModule;
484
+
485
+ if (typeof createParser !== 'function') {
486
+ throw new Error('cli-args-parser does not export createParser');
487
+ }
488
+
489
+ const split = splitWrapperArgs(rawArgs);
490
+ const parser = createParser(WRAPPER_OPTION_SCHEMA);
491
+ const parsed = parser.parse(split.wrapperArgs);
492
+
493
+ if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
494
+ throw new Error(parsed.errors.join('; '));
495
+ }
496
+
497
+ const options = parsed.options || {};
498
+
499
+ return {
500
+ passthroughArgs: split.passthroughArgs,
501
+ rawArgs,
502
+ resolveOptions: {
503
+ assetName: options['asset-name'],
504
+ autoDownload: options['auto-download'] === true,
505
+ binaryPath: options['binary-path'],
506
+ channel: options.channel,
507
+ githubToken: options['github-token'],
508
+ repo: options.repo,
509
+ staticBuild: options['static-build'] === true,
510
+ targetDir: options['target-dir'],
511
+ verify: options.verify !== false,
512
+ version: options.version
513
+ },
514
+ usedDoubleDash: split.usedDoubleDash,
515
+ wrapperOptions: {
516
+ sdkHelp: options['sdk-help'] === true
517
+ }
518
+ };
519
+ }
520
+
521
+ function formatWrapperHelp() {
522
+ return [
523
+ 'redblue-cli wrapper',
524
+ '',
525
+ 'Usage:',
526
+ ' rb [wrapper options] [redblue args]',
527
+ ' npx redblue-cli [redblue args]',
528
+ ' npm exec --package redblue-cli rb -- [redblue args]',
529
+ '',
530
+ 'Wrapper options:',
531
+ ' --binary-path <path> Use an explicit redblue binary',
532
+ ' --target-dir <dir> Resolve or download the binary in this directory',
533
+ ' --auto-download Download the binary if it is missing',
534
+ ' --channel <name> Release channel for downloads (stable, latest, next)',
535
+ ' --version <tag> Pin a release version for downloads',
536
+ ' --asset-name <name> Override the release asset name',
537
+ ' --repo <owner/name> Override the GitHub repository',
538
+ ' --github-token <token> GitHub token for release downloads',
539
+ ' --static-build Prefer static Linux assets when available',
540
+ ' --no-verify Skip SHA256 verification on download',
541
+ ' --sdk-help Show this wrapper help',
542
+ '',
543
+ 'Notes:',
544
+ ' Wrapper options must come before the redblue command.',
545
+ ' The exact command "npx rb" only works when a package named "rb" exists or when this package is already installed and exposes the rb bin.',
546
+ ''
547
+ ].join('\n');
548
+ }
549
+
550
+ function waitForChild(child) {
551
+ return new Promise((resolve, reject) => {
552
+ child.on('error', reject);
553
+ /* node:coverage disable */
554
+ child.on('close', (code, signal) => {
555
+ if (signal) {
556
+ resolve(1);
557
+ return;
558
+ }
559
+ resolve(typeof code === 'number' ? code : 1);
560
+ });
561
+ /* node:coverage enable */
562
+ });
563
+ }
564
+
565
+ async function runCli(argv = process.argv.slice(2), runtime = {}) {
566
+ const stdout = runtime.stdout || process.stdout;
567
+ const stderr = runtime.stderr || process.stderr;
568
+
569
+ try {
570
+ const parsed = await parseWrapperArgs(argv, runtime);
571
+
572
+ if (parsed.wrapperOptions.sdkHelp) {
573
+ stdout.write(formatWrapperHelp());
574
+ return 0;
575
+ }
576
+
577
+ const binaryPath = await resolveBinary(parsed.resolveOptions);
578
+ /* node:coverage disable */
579
+ const spawnOptions = {
580
+ cwd: runtime.cwd || process.cwd(),
581
+ env: Object.assign({}, process.env, runtime.env || {}),
582
+ stdio: runtime.stdio || 'inherit'
583
+ };
584
+ /* node:coverage enable */
585
+ const child = spawnBinary(binaryPath, parsed.passthroughArgs, spawnOptions);
586
+
587
+ return waitForChild(child);
588
+ } catch (error) {
589
+ stderr.write(`redblue-cli: ${error.message}\n`);
590
+ return 1;
591
+ }
592
+ }
593
+
594
+ async function getManifest(options = {}) {
595
+ const binaryPath = await resolveBinary(options);
596
+ const result = await execFilePromise(binaryPath, ['sdk', 'bridge', 'manifest'], options);
597
+ const stdout = String(result.stdout || '').trim();
598
+
599
+ if (!stdout) {
600
+ throw new Error('redblue SDK manifest command returned empty output');
601
+ }
602
+
603
+ try {
604
+ return {
605
+ binaryPath,
606
+ manifest: JSON.parse(stdout)
607
+ };
608
+ } catch (error) {
609
+ const wrapped = new Error(`Failed to parse redblue SDK manifest JSON: ${error.message}`);
610
+ wrapped.stdout = stdout;
611
+ throw wrapped;
612
+ }
613
+ }
614
+
615
+ function findFlag(command, key) {
616
+ return (command.flags || []).find(
617
+ (flag) => flag.long === key || flag.camel_name === key || flag.short === key
618
+ );
619
+ }
620
+
621
+ function buildInvocation(command, route, input, execOptions) {
622
+ const payload = ensureObject(input, 'route input');
623
+ const args = [command.domain, command.resource, route.verb];
624
+ const consumedKeys = new Set();
625
+ const positionals = Array.isArray(route.positionals) ? route.positionals : [];
626
+ const flags = Array.isArray(command.flags) ? command.flags : [];
627
+ const extraFlags = ensureObject(payload.flags, 'flags');
628
+ const preferredFlag =
629
+ command.machine_output && typeof command.machine_output.preferred_flag === 'string'
630
+ ? command.machine_output.preferred_flag
631
+ : null;
632
+ const preferredValue =
633
+ command.machine_output && typeof command.machine_output.preferred_value === 'string'
634
+ ? command.machine_output.preferred_value
635
+ : 'json';
636
+
637
+ for (const positional of positionals) {
638
+ let value;
639
+ if (positional.slot === 'target') {
640
+ value = payload[positional.name];
641
+ if (value === undefined && positional.name !== 'target') {
642
+ value = payload.target;
643
+ }
644
+ consumedKeys.add(positional.name);
645
+ if (payload.target !== undefined) {
646
+ consumedKeys.add('target');
647
+ }
648
+ } else {
649
+ value = payload[positional.name];
650
+ consumedKeys.add(positional.name);
651
+ }
652
+
653
+ if (value === undefined || value === null || value === '') {
654
+ if (positional.required) {
655
+ throw new Error(
656
+ `Missing required positional "${positional.name}" for ${command.domain} ${command.resource} ${route.verb}`
657
+ );
658
+ }
659
+ continue;
660
+ }
661
+
662
+ if (positional.repeated) {
663
+ const values = Array.isArray(value) ? value : [value];
664
+ for (const item of values) {
665
+ args.push(String(item));
666
+ }
667
+ continue;
668
+ }
669
+
670
+ args.push(String(value));
671
+ }
672
+
673
+ if (positionals.length === 0 && payload.target !== undefined && payload.target !== null) {
674
+ args.push(String(payload.target));
675
+ consumedKeys.add('target');
676
+ }
677
+
678
+ const extraArgs = payload.args;
679
+ if (extraArgs !== undefined) {
680
+ if (!Array.isArray(extraArgs)) {
681
+ throw new TypeError('args must be an array when provided');
682
+ }
683
+ for (const item of extraArgs) {
684
+ args.push(String(item));
685
+ }
686
+ consumedKeys.add('args');
687
+ }
688
+
689
+ consumedKeys.add('flags');
690
+
691
+ const explicitMachineFlag = new Set();
692
+
693
+ for (const flag of flags) {
694
+ const longName = flag.long;
695
+ const camelName = flag.camel_name || kebabToCamel(longName);
696
+ let value;
697
+ let usedKey = null;
698
+
699
+ if (Object.prototype.hasOwnProperty.call(payload, longName)) {
700
+ value = payload[longName];
701
+ usedKey = longName;
702
+ } else if (Object.prototype.hasOwnProperty.call(payload, camelName)) {
703
+ value = payload[camelName];
704
+ usedKey = camelName;
705
+ } else if (flag.short && Object.prototype.hasOwnProperty.call(payload, flag.short)) {
706
+ value = payload[flag.short];
707
+ usedKey = flag.short;
708
+ } else if (Object.prototype.hasOwnProperty.call(extraFlags, longName)) {
709
+ value = extraFlags[longName];
710
+ usedKey = `flags.${longName}`;
711
+ } else if (Object.prototype.hasOwnProperty.call(extraFlags, camelName)) {
712
+ value = extraFlags[camelName];
713
+ usedKey = `flags.${camelName}`;
714
+ }
715
+
716
+ if (usedKey) {
717
+ consumedKeys.add(usedKey.split('.')[0]);
718
+ explicitMachineFlag.add(longName);
719
+ }
720
+
721
+ if (value === undefined || value === null || value === false) {
722
+ continue;
723
+ }
724
+
725
+ if (flag.expects_value) {
726
+ if (Array.isArray(value)) {
727
+ for (const item of value) {
728
+ args.push(`--${longName}`, String(item));
729
+ }
730
+ } else {
731
+ args.push(`--${longName}`, String(value));
732
+ }
733
+ } else {
734
+ args.push(`--${longName}`);
735
+ }
736
+ }
737
+
738
+ for (const key of Object.keys(extraFlags)) {
739
+ if (!findFlag(command, key)) {
740
+ throw new Error(
741
+ `Unknown flag "${key}" for ${command.domain} ${command.resource} ${route.verb}`
742
+ );
743
+ }
744
+ }
745
+
746
+ const knownKeys = new Set([
747
+ 'target',
748
+ 'args',
749
+ 'flags',
750
+ 'cwd',
751
+ 'env',
752
+ 'timeout',
753
+ 'maxBuffer',
754
+ 'stdio'
755
+ ]);
756
+
757
+ for (const key of Object.keys(payload)) {
758
+ if (!consumedKeys.has(key) && !knownKeys.has(key) && !findFlag(command, key)) {
759
+ throw new Error(
760
+ `Unknown parameter "${key}" for ${command.domain} ${command.resource} ${route.verb}`
761
+ );
762
+ }
763
+ }
764
+
765
+ const wantsJson = execOptions.json !== false;
766
+ if (wantsJson) {
767
+ args.push('--json');
768
+ if (preferredFlag && !explicitMachineFlag.has(preferredFlag)) {
769
+ args.push(`--${preferredFlag}`, preferredValue);
770
+ }
771
+ }
772
+
773
+ return args;
774
+ }
775
+
776
+ async function invokeJson(binaryPath, command, route, input, execOptions = {}, defaults = {}) {
777
+ const args = buildInvocation(command, route, input, execOptions);
778
+ const result = await execFilePromise(binaryPath, args, {
779
+ cwd: execOptions.cwd || defaults.cwd,
780
+ env: Object.assign({}, defaults.env || {}, execOptions.env || {}),
781
+ timeout: execOptions.timeout || defaults.timeout,
782
+ maxBuffer: execOptions.maxBuffer || defaults.maxBuffer
783
+ });
784
+ const stdout = String(result.stdout || '').trim();
785
+
786
+ if (!stdout) {
787
+ return null;
788
+ }
789
+
790
+ try {
791
+ return JSON.parse(stdout);
792
+ } catch (error) {
793
+ const wrapped = new Error(
794
+ `redblue command did not emit valid JSON for ${command.domain} ${command.resource} ${route.verb}: ${error.message}`
795
+ );
796
+ wrapped.stdout = stdout;
797
+ wrapped.stderr = result.stderr;
798
+ wrapped.args = args;
799
+ throw wrapped;
800
+ }
801
+ }
802
+
803
+ async function invokeRaw(binaryPath, command, route, input, execOptions = {}, defaults = {}) {
804
+ const args = buildInvocation(command, route, input, Object.assign({}, execOptions, { json: false }));
805
+ return execFilePromise(binaryPath, args, {
806
+ cwd: execOptions.cwd || defaults.cwd,
807
+ env: Object.assign({}, defaults.env || {}, execOptions.env || {}),
808
+ timeout: execOptions.timeout || defaults.timeout,
809
+ maxBuffer: execOptions.maxBuffer || defaults.maxBuffer
810
+ });
811
+ }
812
+
813
+ function attachRoute(container, binaryPath, command, route, defaults) {
814
+ const invoke = async function invoke(input = {}, execOptions = {}) {
815
+ return invokeJson(binaryPath, command, route, input, execOptions, defaults);
816
+ };
817
+
818
+ invoke.raw = function raw(input = {}, execOptions = {}) {
819
+ return invokeRaw(binaryPath, command, route, input, execOptions, defaults);
820
+ };
821
+
822
+ invoke.spawn = function spawnRoute(input = {}, spawnOptions = {}) {
823
+ const args = buildInvocation(command, route, input, Object.assign({}, spawnOptions, { json: false }));
824
+ return spawnBinary(binaryPath, args, {
825
+ cwd: spawnOptions.cwd || defaults.cwd,
826
+ env: Object.assign({}, defaults.env || {}, spawnOptions.env || {}),
827
+ stdio: spawnOptions.stdio,
828
+ detached: spawnOptions.detached
829
+ });
830
+ };
831
+
832
+ invoke.meta = { command, route };
833
+ container[route.verb] = invoke;
834
+ }
835
+
836
+ function createDomainProxy(binaryPath, manifest, defaults) {
837
+ const client = {};
838
+
839
+ for (const command of manifest.commands || []) {
840
+ if (!client[command.domain]) {
841
+ client[command.domain] = {};
842
+ }
843
+ if (!client[command.domain][command.resource]) {
844
+ client[command.domain][command.resource] = {};
845
+ }
846
+
847
+ for (const route of command.routes || []) {
848
+ attachRoute(client[command.domain][command.resource], binaryPath, command, route, defaults);
849
+ }
850
+ }
851
+
852
+ return client;
853
+ }
854
+
855
+ async function createClient(options = {}) {
856
+ const defaults = ensureObject(options, 'createClient options');
857
+ const { binaryPath, manifest } = await getManifest(defaults);
858
+ const api = createDomainProxy(binaryPath, manifest, defaults);
859
+
860
+ Object.defineProperties(api, {
861
+ $binaryPath: {
862
+ value: binaryPath,
863
+ enumerable: false
864
+ },
865
+ $manifest: {
866
+ value: manifest,
867
+ enumerable: false
868
+ },
869
+ $downloadBinary: {
870
+ value: downloadBinary,
871
+ enumerable: false
872
+ },
873
+ $resolveBinary: {
874
+ value: resolveBinary,
875
+ enumerable: false
876
+ },
877
+ $exec: {
878
+ value(args, execOptions = {}) {
879
+ if (!Array.isArray(args)) {
880
+ throw new TypeError('$exec expects an array of CLI arguments');
881
+ }
882
+ return execFilePromise(binaryPath, args, Object.assign({}, defaults, execOptions));
883
+ },
884
+ enumerable: false
885
+ },
886
+ $spawn: {
887
+ value(args, spawnOptions = {}) {
888
+ if (!Array.isArray(args)) {
889
+ throw new TypeError('$spawn expects an array of CLI arguments');
890
+ }
891
+ return spawnBinary(binaryPath, args, Object.assign({}, defaults, spawnOptions));
892
+ },
893
+ enumerable: false
894
+ }
895
+ });
896
+
897
+ return api;
898
+ }
899
+
900
+ module.exports = {
901
+ createClient,
902
+ downloadBinary,
903
+ getManifest,
904
+ runCli,
905
+ resolveAssetName,
906
+ resolveBinary
907
+ };
908
+
909
+ module.exports._internal = {
910
+ attachRoute,
911
+ buildInvocation,
912
+ createDomainProxy,
913
+ defaultInstallDir,
914
+ downloadToFile,
915
+ ensureObject,
916
+ execFilePromise,
917
+ exists,
918
+ formatWrapperHelp,
919
+ findFlag,
920
+ getParserCandidatePaths,
921
+ getDefaultBinaryName,
922
+ getReleaseTag,
923
+ invokeJson,
924
+ invokeRaw,
925
+ isExecutable,
926
+ kebabToCamel,
927
+ loadCliArgsParser,
928
+ parseWrapperArgs,
929
+ request,
930
+ requestJson,
931
+ requestText,
932
+ resolveFromPath,
933
+ sha256File,
934
+ splitWrapperArgs,
935
+ spawnBinary,
936
+ toImportSpecifier,
937
+ waitForChild,
938
+ verifyChecksum
939
+ };
940
+
941
+ module.exports.default = module.exports;
942
+
943
+ /* node:coverage disable */
944
+ if (require.main === module) {
945
+ runCli().then((code) => {
946
+ process.exitCode = code;
947
+ });
948
+ }
949
+ /* node:coverage enable */