cloud-doctor 0.0.1-alpha.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.
@@ -0,0 +1,612 @@
1
+ import {
2
+ SsoLoginRequiredError,
3
+ TOP_FIXES_DISPLAY_COUNT,
4
+ buildJsonReport,
5
+ buildJsonReportError,
6
+ computeProjectedScore,
7
+ getDefaultRegistry,
8
+ getTopErrorRules,
9
+ highlighter,
10
+ isProviderId,
11
+ listAwsProfiles,
12
+ loadConfig,
13
+ setColorEnabled,
14
+ sortCategoriesForDisplay,
15
+ validateAwsCredentials
16
+ } from "../chunk-4PZ65VRA.js";
17
+
18
+ // src/cli/index.ts
19
+ import { Command } from "commander";
20
+
21
+ // src/cli/utils/cli-logger.ts
22
+ var cliLogger = {
23
+ log: (message = "") => {
24
+ console.log(message);
25
+ },
26
+ break: () => {
27
+ console.log("");
28
+ },
29
+ error: (message) => {
30
+ console.error(highlighter.error(message));
31
+ },
32
+ warn: (message) => {
33
+ console.warn(highlighter.warn(message));
34
+ },
35
+ info: (message) => {
36
+ console.log(highlighter.info(message));
37
+ }
38
+ };
39
+
40
+ // src/cli/commands/aws-meta.ts
41
+ var awsProfilesAction = async () => {
42
+ const profiles = await listAwsProfiles();
43
+ if (profiles.length === 0) {
44
+ cliLogger.warn("No AWS profiles found in ~/.aws/config or ~/.aws/credentials");
45
+ return;
46
+ }
47
+ cliLogger.log(highlighter.bold("AWS profiles"));
48
+ cliLogger.break();
49
+ for (const profile of profiles) {
50
+ const region = profile.region ? highlighter.dim(` \xB7 ${profile.region}`) : "";
51
+ const kind = highlighter.gray(` (${profile.kind})`);
52
+ cliLogger.log(` ${profile.name}${region}${kind}`);
53
+ if (profile.roleArn) {
54
+ cliLogger.log(highlighter.dim(` role ${profile.roleArn}`));
55
+ }
56
+ }
57
+ cliLogger.break();
58
+ };
59
+ var awsWhoamiAction = async (profile) => {
60
+ const identity = await validateAwsCredentials({
61
+ profileName: profile ?? process.env.AWS_PROFILE,
62
+ region: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION,
63
+ label: profile ?? process.env.AWS_PROFILE ?? "default",
64
+ kind: "profile",
65
+ useEnvironment: !profile && Boolean(process.env.AWS_ACCESS_KEY_ID)
66
+ });
67
+ cliLogger.log(highlighter.bold("AWS identity"));
68
+ cliLogger.break();
69
+ cliLogger.log(` Account ${identity.accountId ?? "unknown"}`);
70
+ cliLogger.log(` ARN ${identity.arn ?? "unknown"}`);
71
+ if (identity.profileName) cliLogger.log(` Profile ${identity.profileName}`);
72
+ if (identity.region) cliLogger.log(` Region ${identity.region}`);
73
+ cliLogger.break();
74
+ };
75
+
76
+ // src/cli/commands/scan.ts
77
+ import ora from "ora";
78
+
79
+ // src/cli/utils/prompts.ts
80
+ import basePrompts from "prompts";
81
+
82
+ // src/cli/utils/unref-stdin.ts
83
+ var unrefStdin = () => {
84
+ if (process.stdin.isTTY) return;
85
+ process.stdin.unref?.();
86
+ };
87
+
88
+ // src/cli/utils/prompts.ts
89
+ var onCancel = () => {
90
+ cliLogger.break();
91
+ cliLogger.log("Cancelled.");
92
+ cliLogger.break();
93
+ process.exit(0);
94
+ };
95
+ var prompts = (questions, options = {}) => basePrompts(questions, { onCancel: options.onCancel ?? onCancel }).finally(unrefStdin);
96
+
97
+ // src/cli/utils/is-non-interactive-environment.ts
98
+ var NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
99
+ "CI",
100
+ "GITHUB_ACTIONS",
101
+ "GITLAB_CI",
102
+ "BUILDKITE",
103
+ "JENKINS_URL",
104
+ "TF_BUILD",
105
+ "CODEBUILD_BUILD_ID",
106
+ "TEAMCITY_VERSION",
107
+ "BITBUCKET_BUILD_NUMBER",
108
+ "CIRCLECI",
109
+ "TRAVIS",
110
+ "DRONE",
111
+ "GIT_DIR"
112
+ ];
113
+ var CODING_AGENT_ENVIRONMENT_VARIABLES = [
114
+ "CURSOR_AGENT",
115
+ "CLAUDE_CODE",
116
+ "CODEX_CI",
117
+ "AGENT_MODE"
118
+ ];
119
+ var isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some(
120
+ (envVariable) => Boolean(process.env[envVariable])
121
+ ) || CODING_AGENT_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable]));
122
+
123
+ // src/cli/utils/should-skip-prompts.ts
124
+ var shouldSkipPrompts = (input = {}) => Boolean(input.yes) || Boolean(input.json) || isNonInteractiveEnvironment() || !process.stdin.isTTY;
125
+
126
+ // src/cli/resolve-identity.ts
127
+ var formatIdentityChoice = (identity) => {
128
+ const hints = [identity.hint, identity.accountId ? `account ${identity.accountId}` : void 0].filter(Boolean).join(" \xB7 ");
129
+ return hints ? `${identity.label} (${hints})` : identity.label;
130
+ };
131
+ var promptForIdentity = async (identities) => {
132
+ const selectable = identities.filter((identity) => !identity.disabled);
133
+ if (selectable.length === 0) {
134
+ throw new Error("No identities available. Configure AWS credentials or pass --profile.");
135
+ }
136
+ const defaultIndex = Math.max(
137
+ 0,
138
+ selectable.findIndex((identity) => identity.profileName === process.env.AWS_PROFILE)
139
+ );
140
+ const { identityId } = await prompts({
141
+ type: "select",
142
+ name: "identityId",
143
+ message: "Which AWS identity?",
144
+ initial: defaultIndex === -1 ? 0 : defaultIndex,
145
+ choices: [
146
+ ...selectable.map((identity) => ({
147
+ title: formatIdentityChoice(identity),
148
+ value: identity.id
149
+ })),
150
+ {
151
+ title: "Enter credentials manually\u2026",
152
+ value: "__manual__"
153
+ }
154
+ ]
155
+ });
156
+ if (identityId === "__manual__") {
157
+ return promptForManualIdentity();
158
+ }
159
+ const selected = selectable.find((identity) => identity.id === identityId);
160
+ if (!selected) throw new Error("Invalid identity selection");
161
+ return selected;
162
+ };
163
+ var promptForManualIdentity = async () => {
164
+ const answers = await prompts([
165
+ {
166
+ type: "text",
167
+ name: "accessKeyId",
168
+ message: "AWS access key ID",
169
+ validate: (value) => value.trim() ? true : "Required"
170
+ },
171
+ {
172
+ type: "password",
173
+ name: "secretAccessKey",
174
+ message: "AWS secret access key",
175
+ validate: (value) => value.trim() ? true : "Required"
176
+ },
177
+ {
178
+ type: "text",
179
+ name: "sessionToken",
180
+ message: "Session token (optional)"
181
+ },
182
+ {
183
+ type: "text",
184
+ name: "region",
185
+ message: "Default region (optional)"
186
+ }
187
+ ]);
188
+ return {
189
+ id: "__manual__",
190
+ label: "(manual credentials)",
191
+ kind: "manual",
192
+ status: "unknown",
193
+ hint: "Session-only \u2014 not saved to disk",
194
+ profileName: void 0,
195
+ defaultRegion: answers.region || void 0
196
+ };
197
+ };
198
+ var identitySummaryToInput = (identity, options, manual) => {
199
+ if (identity.id === "__manual__" && manual) {
200
+ return {
201
+ manualAccessKeyId: manual.accessKeyId,
202
+ manualSecretAccessKey: manual.secretAccessKey,
203
+ manualSessionToken: manual.sessionToken,
204
+ region: options.region ?? identity.defaultRegion,
205
+ accountId: options.account
206
+ };
207
+ }
208
+ if (identity.kind === "environment") {
209
+ return {
210
+ useEnvironment: true,
211
+ region: options.region,
212
+ accountId: options.account
213
+ };
214
+ }
215
+ return {
216
+ profile: options.profile ?? identity.profileName,
217
+ region: options.region ?? identity.defaultRegion,
218
+ accountId: options.account
219
+ };
220
+ };
221
+ var resolveIdentity = async (options) => {
222
+ const config = await loadConfig();
223
+ const ctx = {
224
+ cwd: process.cwd(),
225
+ yes: Boolean(options.yes),
226
+ json: Boolean(options.json)
227
+ };
228
+ const configProfile = config.aws?.profile;
229
+ const identities = await options.plugin.discoverIdentities(ctx);
230
+ let identityInput = {
231
+ profile: options.profile ?? configProfile,
232
+ region: options.region,
233
+ accountId: options.account ?? config.aws?.account
234
+ };
235
+ if (!identityInput.profile && !identityInput.useEnvironment && !shouldSkipPrompts({ yes: options.yes, json: options.json })) {
236
+ const selected = await promptForIdentity(identities);
237
+ if (selected.id === "__manual__") {
238
+ const manualAnswers = await prompts([
239
+ {
240
+ type: "text",
241
+ name: "accessKeyId",
242
+ message: "AWS access key ID",
243
+ validate: (value) => value.trim() ? true : "Required"
244
+ },
245
+ {
246
+ type: "password",
247
+ name: "secretAccessKey",
248
+ message: "AWS secret access key",
249
+ validate: (value) => value.trim() ? true : "Required"
250
+ },
251
+ {
252
+ type: "text",
253
+ name: "sessionToken",
254
+ message: "Session token (optional)"
255
+ }
256
+ ]);
257
+ identityInput = identitySummaryToInput(selected, options, manualAnswers);
258
+ } else {
259
+ identityInput = identitySummaryToInput(selected, options);
260
+ }
261
+ }
262
+ const resolved = await options.plugin.resolveIdentity(ctx, identityInput);
263
+ if (resolved.kind === "manual" && identityInput.manualAccessKeyId) {
264
+ const validated = await validateAwsCredentials({
265
+ profileName: void 0,
266
+ region: identityInput.region,
267
+ label: "(manual credentials)",
268
+ kind: "manual",
269
+ manualAccessKeyId: identityInput.manualAccessKeyId,
270
+ manualSecretAccessKey: identityInput.manualSecretAccessKey,
271
+ manualSessionToken: identityInput.manualSessionToken
272
+ });
273
+ return { identity: validated, identityInput };
274
+ }
275
+ try {
276
+ const validated = await options.plugin.validateIdentity(resolved);
277
+ if (options.account && validated.accountId && validated.accountId !== options.account) {
278
+ throw new Error(
279
+ `Account guard failed: expected ${options.account}, got ${validated.accountId}`
280
+ );
281
+ }
282
+ return { identity: validated, identityInput };
283
+ } catch (error) {
284
+ if (error instanceof SsoLoginRequiredError) {
285
+ console.error(highlighter.error(error.message));
286
+ process.exit(1);
287
+ }
288
+ throw error;
289
+ }
290
+ };
291
+
292
+ // src/cli/resolve-provider.ts
293
+ var resolveProvider = async (input) => {
294
+ const registry = getDefaultRegistry();
295
+ const config = await loadConfig();
296
+ if (input.providerArg) {
297
+ if (!isProviderId(input.providerArg)) {
298
+ throw new Error(
299
+ `Unknown provider "${input.providerArg}". Expected one of: aws, gcloud, k8s, db, cloudflare`
300
+ );
301
+ }
302
+ return registry.require(input.providerArg);
303
+ }
304
+ const configured = config.defaultProvider;
305
+ if (configured) {
306
+ return registry.require(configured);
307
+ }
308
+ if (shouldSkipPrompts(input)) {
309
+ return registry.require("aws");
310
+ }
311
+ const plugins = registry.list();
312
+ const { provider } = await prompts({
313
+ type: "select",
314
+ name: "provider",
315
+ message: "What do you want to grade?",
316
+ initial: 0,
317
+ choices: plugins.map((plugin) => ({
318
+ title: plugin.status === "coming-soon" ? `${plugin.displayName} (coming soon)` : plugin.displayName,
319
+ description: plugin.description,
320
+ value: plugin.id,
321
+ disabled: plugin.status === "coming-soon"
322
+ }))
323
+ });
324
+ const selected = registry.require(provider);
325
+ if (selected.status === "coming-soon") {
326
+ throw new Error(`${selected.displayName} support is coming soon. Use "aws" for now.`);
327
+ }
328
+ return selected;
329
+ };
330
+
331
+ // src/cli/render-scan-report.ts
332
+ import isUnicodeSupported from "is-unicode-supported";
333
+ var POINTER = isUnicodeSupported() ? "\u203A" : ">";
334
+ var renderScoreBar = (score, width = 24) => {
335
+ const filled = Math.round(score / 100 * width);
336
+ const filledSegment = highlighter.success("\u2588".repeat(filled));
337
+ const emptySegment = highlighter.dim("\u2591".repeat(Math.max(0, width - filled)));
338
+ return `${filledSegment}${emptySegment}`;
339
+ };
340
+ var renderScanReport = (result, verbose) => {
341
+ const { summary, identity, provider, diagnostics, meta } = result;
342
+ const projectedScore = computeProjectedScore(diagnostics) ?? summary.projectedScore;
343
+ const totalFindings = summary.errorCount + summary.warningCount;
344
+ console.log("");
345
+ console.log(
346
+ ` ${highlighter.bold(`${summary.score} / 100`)} ${summary.label} ${totalFindings} findings`
347
+ );
348
+ console.log(` ${renderScoreBar(summary.score)}`);
349
+ if (projectedScore !== void 0 && projectedScore > summary.score) {
350
+ console.log(
351
+ highlighter.dim(
352
+ ` +${projectedScore - summary.score} by fixing the top ${TOP_FIXES_DISPLAY_COUNT} issues`
353
+ )
354
+ );
355
+ }
356
+ console.log("");
357
+ console.log(
358
+ highlighter.dim(
359
+ `${provider.toUpperCase()} \xB7 account ${identity.accountId ?? identity.id} \xB7 ${identity.label}`
360
+ )
361
+ );
362
+ console.log("");
363
+ const topRules = getTopErrorRules(diagnostics);
364
+ if (topRules.length > 0) {
365
+ console.log(highlighter.bold(` Top ${topRules.length} issues to fix first`));
366
+ console.log("");
367
+ topRules.forEach(({ ruleKey, diagnostics: ruleDiagnostics }, index) => {
368
+ const sample = ruleDiagnostics[0];
369
+ if (!sample) return;
370
+ console.log(
371
+ ` ${index + 1}. ${highlighter.bold(`${sample.category}: ${sample.title}`)}`
372
+ );
373
+ const location = [sample.region, sample.resourceArn].filter(Boolean).join(" \xB7 ");
374
+ if (location) console.log(highlighter.dim(` ${location}`));
375
+ console.log(` ${POINTER} ${sample.recommendation}`);
376
+ if (verbose && ruleDiagnostics.length > 1) {
377
+ console.log(highlighter.gray(` (${ruleDiagnostics.length} resources)`));
378
+ }
379
+ console.log("");
380
+ });
381
+ } else if (totalFindings === 0) {
382
+ console.log(highlighter.success(" No issues found."));
383
+ console.log("");
384
+ }
385
+ if (totalFindings > 0) {
386
+ console.log(highlighter.bold(` All ${totalFindings} issues`));
387
+ const categories = sortCategoriesForDisplay(
388
+ Object.keys(summary.categoryCounts)
389
+ );
390
+ for (const category of categories) {
391
+ const counts = summary.categoryCounts[category];
392
+ if (!counts) continue;
393
+ const parts = [];
394
+ if (counts.errors > 0) parts.push(`${counts.errors} errors`);
395
+ if (counts.warnings > 0) parts.push(`${counts.warnings} warnings`);
396
+ console.log(` ${category.padEnd(16)} ${POINTER} ${parts.join(", ")}`);
397
+ }
398
+ console.log("");
399
+ }
400
+ const regionInfo = meta.regionsScanned && meta.regionsScanned.length > 0 ? ` \xB7 ${meta.regionsScanned.length} regions` : "";
401
+ console.log(
402
+ highlighter.dim(
403
+ ` Scanned in ${(meta.durationMs / 1e3).toFixed(1)}s${regionInfo} \xB7 read-only \xB7 no changes made`
404
+ )
405
+ );
406
+ if (!verbose && totalFindings > 0) {
407
+ console.log(highlighter.dim(" Run npx cloud-doctor@latest --verbose for every finding"));
408
+ }
409
+ console.log("");
410
+ };
411
+
412
+ // src/cli/utils/json-mode.ts
413
+ import { performance } from "perf_hooks";
414
+ var context = null;
415
+ var installSilentConsole = () => {
416
+ const noop = () => {
417
+ };
418
+ for (const key of ["log", "error", "warn", "info", "debug", "trace"]) {
419
+ console[key] = noop;
420
+ }
421
+ };
422
+ var serialize = (value) => context?.compact ? JSON.stringify(value) : JSON.stringify(value, null, 2);
423
+ var enableJsonMode = (compact) => {
424
+ context = { compact, startTime: performance.now() };
425
+ installSilentConsole();
426
+ };
427
+ var isJsonModeActive = () => context !== null;
428
+ var writeJsonReport = (report) => {
429
+ process.stdout.write(`${serialize(report)}
430
+ `);
431
+ };
432
+ var writeJsonErrorReport = (error) => {
433
+ if (!context) return;
434
+ const report = buildJsonReportError({
435
+ error,
436
+ elapsedMilliseconds: performance.now() - context.startTime
437
+ });
438
+ process.stdout.write(`${serialize(report)}
439
+ `);
440
+ };
441
+
442
+ // src/cli/commands/scan.ts
443
+ var scanAction = async (options) => {
444
+ const config = await loadConfig();
445
+ const plugin = await resolveProvider({
446
+ providerArg: options.providerArg,
447
+ yes: options.yes,
448
+ json: options.json
449
+ });
450
+ if (plugin.status === "coming-soon") {
451
+ throw new Error(`${plugin.displayName} support is coming soon. Use "aws" for now.`);
452
+ }
453
+ const spinner = !isJsonModeActive() && !isNonInteractiveEnvironment() ? ora(`Connecting to ${plugin.displayName}\u2026`).start() : null;
454
+ try {
455
+ const { identity } = await resolveIdentity({
456
+ plugin,
457
+ profile: options.profile ?? config.aws?.profile,
458
+ region: options.region,
459
+ account: options.account ?? config.aws?.account,
460
+ yes: options.yes,
461
+ json: options.json
462
+ });
463
+ if (spinner) {
464
+ spinner.text = `Grading ${plugin.displayName} account ${identity.accountId ?? identity.id}\u2026`;
465
+ }
466
+ const regions = options.regions?.split(",").map((r) => r.trim()) ?? config.aws?.regions;
467
+ const result = await plugin.scan(identity, {
468
+ regions,
469
+ verbose: Boolean(options.verbose),
470
+ accountGuard: options.account
471
+ });
472
+ spinner?.stop();
473
+ if (isJsonModeActive()) {
474
+ writeJsonReport(buildJsonReport(result));
475
+ return;
476
+ }
477
+ renderScanReport(result, Boolean(options.verbose));
478
+ } catch (error) {
479
+ spinner?.stop();
480
+ throw error;
481
+ }
482
+ };
483
+
484
+ // src/cli/utils/apply-color-preference.ts
485
+ var parseColorPreference = (colorOption) => {
486
+ if (colorOption === true) return true;
487
+ if (colorOption === false) return false;
488
+ return void 0;
489
+ };
490
+ var applyColorPreference = (colorOption) => {
491
+ const preference = parseColorPreference(colorOption);
492
+ if (preference === void 0) return;
493
+ setColorEnabled(preference);
494
+ };
495
+ var formatExampleLines = (examples) => {
496
+ const width = Math.max(...examples.map(([command]) => command.length));
497
+ return examples.map(
498
+ ([command, description]) => ` $ ${command.padEnd(width)} ${highlighter.dim(`# ${description}`)}`
499
+ ).join("\n");
500
+ };
501
+
502
+ // src/cli/utils/version.ts
503
+ var VERSION = "0.0.1-alpha.0";
504
+ var TERMINAL_HANGUP_EXIT_CODE = 130;
505
+
506
+ // src/cli/utils/exit-gracefully.ts
507
+ var exitGracefully = () => {
508
+ process.exit(TERMINAL_HANGUP_EXIT_CODE);
509
+ };
510
+
511
+ // src/cli/utils/guard-stdin.ts
512
+ var TERMINAL_HANGUP_CODES = /* @__PURE__ */ new Set(["EIO", "ENXIO"]);
513
+ var handleStdinError = (error) => {
514
+ if (error.code !== void 0 && TERMINAL_HANGUP_CODES.has(error.code)) {
515
+ process.exit(TERMINAL_HANGUP_EXIT_CODE);
516
+ }
517
+ throw error;
518
+ };
519
+ var guardStdin = () => {
520
+ process.stdin.on("error", handleStdinError);
521
+ };
522
+
523
+ // src/cli/index.ts
524
+ process.on("SIGINT", exitGracefully);
525
+ process.on("SIGTERM", exitGracefully);
526
+ unrefStdin();
527
+ guardStdin();
528
+ var renderRootHelpEpilog = () => `
529
+ ${highlighter.dim("Examples:")}
530
+ ${formatExampleLines([
531
+ ["cloud-doctor", "pick a provider, then grade (defaults to AWS)"],
532
+ ["cloud-doctor aws", "grade your AWS account"],
533
+ ["cloud-doctor aws --profile prod", "use a named AWS profile"],
534
+ ["cloud-doctor aws --yes --json", "non-interactive JSON report"],
535
+ ["cloud-doctor aws profiles", "list discovered AWS profiles"],
536
+ ["cloud-doctor aws whoami", "show the resolved AWS identity"]
537
+ ])}
538
+
539
+ ${highlighter.dim("Configuration:")}
540
+ Add a ${highlighter.info("doctor.config.ts")} (or a ${highlighter.info('"cloudDoctor"')} key in package.json).
541
+ `;
542
+ var program = new Command().name("cloud-doctor").description("Grade your cloud account \u2014 misconfigurations, observability, and security posture").version(VERSION, "-v, --version", "display the version number").option("--yes, -y", "skip interactive prompts", false).option("--json", "write a machine-readable report to stdout", false).option("--verbose", "show every finding", false).option("--no-color", "disable ANSI colors").hook("preAction", (thisCommand) => {
543
+ const opts = thisCommand.opts();
544
+ applyColorPreference(opts.color);
545
+ if (thisCommand.opts().json) {
546
+ enableJsonMode(false);
547
+ }
548
+ }).addHelpText("after", renderRootHelpEpilog);
549
+ var addScanOptions = (command) => command.option("--profile <name>", "AWS named profile (or provider-specific identity)").option("--region <region>", "default region for discovery").option("--regions <list>", "comma-separated regions to scan").option("--account <id>", "assert the expected account ID");
550
+ var collectOptions = (command) => {
551
+ const parent = command.parent;
552
+ const root = parent?.parent ?? parent;
553
+ return { ...root?.opts() ?? {}, ...parent?.opts() ?? {}, ...command.opts() };
554
+ };
555
+ var runScan = async (providerArg, command) => {
556
+ const opts = collectOptions(command);
557
+ try {
558
+ await scanAction({
559
+ providerArg,
560
+ profile: opts.profile,
561
+ region: opts.region,
562
+ account: opts.account,
563
+ regions: opts.regions,
564
+ verbose: Boolean(opts.verbose),
565
+ yes: Boolean(opts.yes),
566
+ json: Boolean(opts.json)
567
+ });
568
+ } catch (error) {
569
+ if (isJsonModeActive()) {
570
+ writeJsonErrorReport(error);
571
+ process.exit(1);
572
+ }
573
+ const message = error instanceof Error ? error.message : String(error);
574
+ console.error(highlighter.error(message));
575
+ process.exit(1);
576
+ }
577
+ };
578
+ addScanOptions(
579
+ program.command("scan [provider]").description("grade a cloud account").action(async (provider, _options, command) => {
580
+ await runScan(provider, command);
581
+ })
582
+ );
583
+ program.argument("[provider]", "cloud provider (aws, gcloud, k8s, db, cloudflare)").option("--profile <name>", "AWS named profile (or provider-specific identity)").option("--region <region>", "default region for discovery").option("--regions <list>", "comma-separated regions to scan").option("--account <id>", "assert the expected account ID").action(async (provider, _options, command) => {
584
+ await runScan(provider, command);
585
+ });
586
+ var aws = program.command("aws").description("AWS account commands");
587
+ addScanOptions(
588
+ aws.command("scan").description("grade an AWS account").action(async (_options, command) => {
589
+ await runScan("aws", command);
590
+ })
591
+ );
592
+ aws.command("profiles").description("list AWS profiles from ~/.aws/config").action(async () => {
593
+ await awsProfilesAction();
594
+ });
595
+ aws.command("whoami").description("show the resolved AWS identity").option("--profile <name>", "AWS named profile").action(async (options) => {
596
+ try {
597
+ await awsWhoamiAction(options.profile);
598
+ } catch (error) {
599
+ const message = error instanceof Error ? error.message : String(error);
600
+ console.error(highlighter.error(message));
601
+ process.exit(1);
602
+ }
603
+ });
604
+ for (const provider of ["gcloud", "k8s", "db", "cloudflare"]) {
605
+ addScanOptions(
606
+ program.command(provider).description(`grade ${provider} (coming soon)`).action(async (_options, command) => {
607
+ await runScan(provider, command);
608
+ })
609
+ );
610
+ }
611
+ await program.parseAsync(process.argv);
612
+ //# sourceMappingURL=index.js.map