@maverick006/vibeguard 1.0.10 → 1.0.12

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/src/index.ts CHANGED
@@ -1,211 +1,455 @@
1
- #!/usr/bin/env node
2
- import 'dotenv/config';
3
- import { Command } from 'commander';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import { ContextualExplainer } from '@maverick006/ai-engine';
7
- import { NormalizedFinding, Severity } from '@maverick006/types';
8
- import { readFileSync, existsSync } from 'fs';
9
- import { join } from 'path';
10
- import { renderDashboard, calculateScore, getGitInfo } from './formatter';
11
-
12
- const program = new Command();
13
-
14
- program
15
- .name('vibeguard')
16
- .description('AI-Powered DevSecOps Orchestrator CLI')
17
- .version('1.0.10');
18
-
19
- program
20
- .command('scan [path]')
21
- .description('Run a security scan on the current directory or target path')
22
- .option('-d, --dir <path>', 'Directory to scan', process.cwd())
23
- .option('--fix', 'Automatically generate AI remediation fixes and interactive diff')
24
- .option('--ci', 'Run in non-interactive CI mode and exit with policy status code')
25
- .action(async (targetPath, options) => {
26
- const scanDir = targetPath || options.dir || process.cwd();
27
- const startTime = Date.now();
28
- let hasSystemError = false;
29
-
30
- // Use a minimal spinner if in CI mode, or bypass ora entirely if preferred.
31
- // For simplicity, we just won't render the interactive dashboard if --ci is set.
32
- const spinner = ora({
33
- text: chalk.hex('#00E5FF')('Scanning repository for vulnerabilities (SAST, SCA, Secrets, IaC)...'),
34
- spinner: 'dots',
35
- isSilent: options.ci
36
- }).start();
37
-
38
- let findings: NormalizedFinding[] = [];
39
-
40
- try {
41
- // Initialize the security orchestrator and scanners
42
- const { Orchestrator } = require('@maverick006/security-engine');
43
- const scanners: any[] = [];
44
-
45
- try {
46
- const { NpmAuditScanner } = require('@maverick006/scanner-npm-audit');
47
- scanners.push(new NpmAuditScanner());
48
- } catch (e) {}
49
-
50
- try {
51
- const { TrivyScanner } = require('@maverick006/scanner-trivy');
52
- scanners.push(new TrivyScanner());
53
- } catch (e) {}
54
-
55
- try {
56
- const { SemgrepScanner } = require('@maverick006/scanner-semgrep');
57
- scanners.push(new SemgrepScanner());
58
- } catch (e) {}
59
-
60
- try {
61
- const { GitleaksScanner } = require('@maverick006/scanner-gitleaks');
62
- scanners.push(new GitleaksScanner());
63
- } catch (e) {}
64
-
65
- try {
66
- const { CheckovScanner } = require('@maverick006/scanner-checkov');
67
- scanners.push(new CheckovScanner());
68
- } catch (e) {}
69
-
70
- try {
71
- const { ZapScanner } = require('@maverick006/scanner-zap');
72
- scanners.push(new ZapScanner());
73
- } catch (e) {}
74
-
75
- const orchestrator = new Orchestrator(scanners);
76
-
77
- const scanResult = await orchestrator.runScan({
78
- scanId: `scan-${Date.now()}`,
79
- repositoryUrl: 'local',
80
- repositoryPath: scanDir,
81
- branch: 'main'
82
- });
83
-
84
- findings = scanResult.findings || [];
85
- } catch (err) {
86
- console.error(chalk.red('Orchestrator failed to run scans.'), err);
87
- hasSystemError = true;
88
- }
89
-
90
- spinner.stop();
91
-
92
- const elapsedMs = Date.now() - startTime;
93
- const duration = elapsedMs > 60000
94
- ? `${Math.floor(elapsedMs / 60000)}m ${Math.floor((elapsedMs % 60000) / 1000)}s`
95
- : `${(elapsedMs / 1000).toFixed(1)}s`;
96
-
97
- const stats = calculateScore(findings);
98
- const gitInfo = getGitInfo(scanDir);
99
-
100
- let remediationData: any = undefined;
101
-
102
- // Generate AI Remediation only if not in CI mode to save time, or if explicitly asked
103
- if (findings.length > 0 && !options.ci && (options.fix || true)) {
104
- try {
105
- const explainer = new ContextualExplainer();
106
- const primaryFinding = findings[0];
107
-
108
- let snippet = primaryFinding.codeSnippet || '';
109
- if (primaryFinding.file && existsSync(join(scanDir, primaryFinding.file))) {
110
- const content = readFileSync(join(scanDir, primaryFinding.file), 'utf-8');
111
- const lines = content.split('\n');
112
- const targetLine = primaryFinding.line || 1;
113
- snippet = lines.slice(Math.max(0, targetLine - 3), targetLine + 3).join('\n');
114
- }
115
-
116
- const explanation = await explainer.explainFinding(primaryFinding, { codeContext: snippet });
117
-
118
- remediationData = {
119
- findingId: primaryFinding.id,
120
- issue: explanation.summary,
121
- impact: explanation.details || 'Potential security impact based on context.',
122
- recommendation: explanation.remediation,
123
- diffSnippet: explanation.codeFix || '',
124
- confidence: 90
125
- };
126
- } catch (e: any) {
127
- if (!options.ci) {
128
- console.error(chalk.yellow('AI Remediation generation failed.'), e);
129
- }
130
- }
131
- }
132
-
133
- if (!options.ci) {
134
- // Render the beautiful cyber dashboard interactively
135
- renderDashboard({
136
- findings,
137
- stats,
138
- gitInfo,
139
- duration,
140
- remediation: remediationData
141
- });
142
- } else {
143
- // CI Output
144
- console.log(`VibeGuard CI Scan Complete. Duration: ${duration}`);
145
- console.log(`Findings: ${findings.length}`);
146
- console.log(`Risk Score: ${stats.score}/100 (${stats.riskLevel})`);
147
- const critical = findings.filter(f => (f.severity || '').toUpperCase() === Severity.CRITICAL).length;
148
- const high = findings.filter(f => (f.severity || '').toUpperCase() === Severity.HIGH).length;
149
- console.log(`Critical: ${critical}, High: ${high}`);
150
- }
151
-
152
- // Sync with Web Dashboard
153
- const syncSpinner = ora({
154
- text: chalk.dim('Syncing results to VibeGuard Dashboard...'),
155
- spinner: 'dots',
156
- isSilent: options.ci
157
- }).start();
158
-
159
- try {
160
- const API_URL = process.env.VIBEGUARD_API_URL || 'https://vibeguard-eep3.onrender.com';
161
- const API_KEY = process.env.VIBEGUARD_API_KEY || 'dev-api-key-123';
162
- const repoName = gitInfo.name || 'Local Project';
163
- const repoUrl = (gitInfo as any).remoteUrl || gitInfo.name || 'local';
164
-
165
- const response = await fetch(`${API_URL}/api/scans/upload`, {
166
- method: 'POST',
167
- headers: {
168
- 'Content-Type': 'application/json',
169
- 'Authorization': `Bearer ${API_KEY}`
170
- },
171
- body: JSON.stringify({
172
- repositoryName: repoName,
173
- repositoryUrl: repoUrl,
174
- numericScore: stats.score,
175
- score: stats.riskLevel,
176
- findings: findings
177
- })
178
- });
179
-
180
- if (response.ok) {
181
- const dashboardUrl = `https://vibeguard-web-eight.vercel.app/dashboard?repo=${encodeURIComponent(repoName)}`;
182
- syncSpinner.succeed(chalk.green(`Results synced to dashboard: ${chalk.cyan.underline(dashboardUrl)}`));
183
- } else {
184
- syncSpinner.fail(chalk.red(`Failed to sync results to dashboard (${response.status} ${response.statusText}).`));
185
- }
186
- } catch (e) {
187
- syncSpinner.warn(chalk.yellow('Dashboard API unreachable. Skipping sync.'));
188
- }
189
-
190
- // Exit codes
191
- if (hasSystemError) {
192
- process.exit(2);
193
- }
194
-
195
- // Policy fail if we have CRITICAL or HIGH findings
196
- const criticalOrHighCount = findings.filter(f => {
197
- const s = (f.severity || '').toUpperCase();
198
- return s === Severity.CRITICAL || s === Severity.HIGH;
199
- }).length;
200
-
201
- if (criticalOrHighCount > 0) {
202
- if (options.ci) console.error(chalk.red('Security Policy FAILED.'));
203
- process.exit(1);
204
- }
205
-
206
- if (options.ci) console.log(chalk.green('Security Policy PASSED.'));
207
- process.exit(0);
208
- });
209
-
210
- program.parse(process.argv);
211
-
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { ContextualExplainer } from '@maverick006/ai-engine';
7
+ import { calculateScore as calculateEngineScore } from '@maverick006/security-engine';
8
+ import { NormalizedFinding, Severity, ScannerCoverage } from '@maverick006/types';
9
+ import { readFileSync, existsSync } from 'fs';
10
+ import { join, resolve } from 'path';
11
+ import {
12
+ renderDashboard,
13
+ calculateScore,
14
+ getGitInfo,
15
+ AIRemediationData,
16
+ ScannerTelemetry,
17
+ renderCIOutput,
18
+ generateJsonOutput,
19
+ evaluatePolicy
20
+ } from './formatter';
21
+ import { loadCredentials, saveCredentials, clearCredentials, getCredentialsPath } from './credentials';
22
+ import readline from 'readline';
23
+
24
+ const program = new Command();
25
+
26
+ program
27
+ .name('vibeguard')
28
+ .description('VIBEGUARD: Cloud + Security Posture CLI')
29
+ .version('1.0.12');
30
+
31
+ program
32
+ .command('scan [path]')
33
+ .description('Run a security scan on the current directory or target path')
34
+ .option('-d, --dir <path>', 'Directory to scan', process.cwd())
35
+ .option('--fix', 'Generate optional advisory AI remediation suggestion')
36
+ .option('--ci', 'Run in non-interactive CI mode and exit with policy status code')
37
+ .option('--json', 'Output machine-readable JSON summary')
38
+ .option('-v, --verbose', 'Show detailed scanner telemetry, install hints, and diagnostics')
39
+ .option('--fail-on <severity>', 'Severity threshold to trigger non-zero exit in CI (critical, high, medium, low)', 'high')
40
+ .option('--sync', 'Sync scan telemetry and findings with authenticated VibeGuard Cloud account')
41
+ .action(async (targetPath, options) => {
42
+ const scanDir = resolve(targetPath || options.dir || process.cwd());
43
+ const startTime = Date.now();
44
+ let hasSystemError = false;
45
+
46
+ const isJson = Boolean(options.json);
47
+ const isSilent = options.ci || isJson;
48
+
49
+ const spinner = ora({
50
+ text: chalk.hex('#00E5FF')('Orchestrating security scanners across repository...'),
51
+ spinner: 'dots',
52
+ isSilent
53
+ }).start();
54
+
55
+ let findings: NormalizedFinding[] = [];
56
+ let scannerResults: any[] = [];
57
+ let coverageData: ScannerCoverage = {
58
+ code: false,
59
+ dependencies: false,
60
+ secrets: false,
61
+ containers: false,
62
+ iac: false,
63
+ web: false,
64
+ cloud: false
65
+ };
66
+
67
+ try {
68
+ const { Orchestrator } = require('@maverick006/security-engine');
69
+ const scanners: any[] = [];
70
+
71
+ try {
72
+ const { SemgrepScanner } = require('@maverick006/scanner-semgrep');
73
+ scanners.push(new SemgrepScanner());
74
+ } catch {}
75
+
76
+ try {
77
+ const { GitleaksScanner } = require('@maverick006/scanner-gitleaks');
78
+ scanners.push(new GitleaksScanner());
79
+ } catch {}
80
+
81
+ try {
82
+ const { NpmAuditScanner } = require('@maverick006/scanner-npm-audit');
83
+ scanners.push(new NpmAuditScanner());
84
+ } catch {}
85
+
86
+ try {
87
+ const { TrivyScanner } = require('@maverick006/scanner-trivy');
88
+ scanners.push(new TrivyScanner());
89
+ } catch {}
90
+
91
+ try {
92
+ const { CheckovScanner } = require('@maverick006/scanner-checkov');
93
+ scanners.push(new CheckovScanner());
94
+ } catch {}
95
+
96
+ try {
97
+ const { ZapScanner } = require('@maverick006/scanner-zap');
98
+ scanners.push(new ZapScanner());
99
+ } catch {}
100
+
101
+ try {
102
+ const { AwsCspmScanner } = require('@maverick006/scanner-aws-cspm');
103
+ scanners.push(new AwsCspmScanner());
104
+ } catch {}
105
+
106
+ const orchestrator = new Orchestrator(scanners, { concurrencyLimit: 3 });
107
+
108
+ const scanResult = await orchestrator.runScan({
109
+ scanId: `scan-${Date.now()}`,
110
+ repositoryPath: scanDir,
111
+ branch: 'main'
112
+ });
113
+
114
+ findings = scanResult.findings || [];
115
+ scannerResults = scanResult.scannerResults || [];
116
+ coverageData = scanResult.coverage || coverageData;
117
+ } catch (err: any) {
118
+ if (!isJson) {
119
+ console.error(chalk.red('Orchestrator encountered an execution error:'), err.message || err);
120
+ }
121
+ hasSystemError = true;
122
+ }
123
+
124
+ spinner.stop();
125
+
126
+ const elapsedMs = Date.now() - startTime;
127
+ const duration = elapsedMs > 60000
128
+ ? `${Math.floor(elapsedMs / 60000)}m ${Math.floor((elapsedMs % 60000) / 1000)}s`
129
+ : `${(elapsedMs / 1000).toFixed(1)}s`;
130
+
131
+ // Ensure all findings follow the consistent VG-FIND-001 format
132
+ findings.forEach((f, idx) => {
133
+ if (!f.id || !f.id.startsWith('VG-FIND-')) {
134
+ f.id = `VG-FIND-${String(idx + 1).padStart(3, '0')}`;
135
+ }
136
+ });
137
+
138
+ const deterministicScore = calculateEngineScore(findings, coverageData);
139
+ const stats = calculateScore(findings);
140
+ const gitInfo = getGitInfo(scanDir);
141
+
142
+ // Structure AI remediation (advisory only)
143
+ let remediationData: AIRemediationData | undefined = undefined;
144
+
145
+ if (findings.length > 0 && !options.ci && !isJson && (options.fix || true)) {
146
+ const primaryFinding = findings[0];
147
+ const hasAiKey = Boolean(process.env.NVIDIA_API_KEY);
148
+
149
+ if (!hasAiKey) {
150
+ remediationData = {
151
+ findingId: primaryFinding.id || 'VG-FIND-001',
152
+ severity: primaryFinding.severity,
153
+ issue: primaryFinding.title || 'Security finding identified',
154
+ hasConcretePatch: false,
155
+ status: 'UNAVAILABLE',
156
+ unavailableReason: 'NVIDIA_API_KEY not configured.'
157
+ };
158
+ } else {
159
+ try {
160
+ const explainer = new ContextualExplainer();
161
+ let snippet = primaryFinding.codeSnippet || '';
162
+ if (primaryFinding.file && existsSync(join(scanDir, primaryFinding.file))) {
163
+ const content = readFileSync(join(scanDir, primaryFinding.file), 'utf-8');
164
+ const lines = content.split('\n');
165
+ const targetLine = primaryFinding.line || 1;
166
+ snippet = lines.slice(Math.max(0, targetLine - 3), targetLine + 3).join('\n');
167
+ }
168
+
169
+ const explanation = await explainer.explainFinding(primaryFinding, { codeContext: snippet });
170
+ const hasConcretePatch = Boolean(
171
+ explanation.codeFix &&
172
+ explanation.codeFix.trim() &&
173
+ !explanation.codeFix.toLowerCase().includes('no patch')
174
+ );
175
+
176
+ remediationData = {
177
+ findingId: primaryFinding.id || 'VG-FIND-001',
178
+ severity: primaryFinding.severity,
179
+ issue: explanation.summary,
180
+ impact: explanation.details,
181
+ recommendation: explanation.remediation,
182
+ suggestedFix: hasConcretePatch ? explanation.codeFix : undefined,
183
+ hasConcretePatch,
184
+ confidence: hasConcretePatch ? 90 : undefined,
185
+ status: hasConcretePatch ? 'AWAITING REVIEW' : 'GUIDANCE ONLY'
186
+ };
187
+ } catch {
188
+ remediationData = {
189
+ findingId: primaryFinding.id || 'VG-FIND-001',
190
+ severity: primaryFinding.severity,
191
+ issue: primaryFinding.title,
192
+ hasConcretePatch: false,
193
+ status: 'UNAVAILABLE',
194
+ unavailableReason: 'AI service temporarily unavailable.'
195
+ };
196
+ }
197
+ }
198
+ }
199
+
200
+ // Policy verification
201
+ const failThreshold = (options.failOn || 'high').toLowerCase();
202
+ const policyEvaluation = evaluatePolicy(failThreshold, deterministicScore.breakdown, findings.length);
203
+ const policyPassed = policyEvaluation.passed;
204
+ const thresholdBreached = !policyPassed;
205
+
206
+ // Cloud synchronization tracking
207
+ let syncStatus: 'SYNCED' | 'SKIPPED' | 'FAILED' = 'SKIPPED';
208
+ if (options.sync) {
209
+ const creds = loadCredentials();
210
+ if (!creds || !creds.token) {
211
+ syncStatus = 'FAILED';
212
+ if (!isJson && !options.ci) {
213
+ console.log(chalk.red('\n✖ Authentication required for cloud sync.'));
214
+ console.log(chalk.yellow(" Run 'vibeguard login' to authenticate with VibeGuard Cloud, or omit --sync for 100% offline local scanning.\n"));
215
+ }
216
+ } else {
217
+ try {
218
+ const API_URL = creds.apiUrl || process.env.VIBEGUARD_API_URL || 'https://vibeguard-eep3.onrender.com';
219
+ const repoName = gitInfo.name || 'Local Project';
220
+ const repoUrl = (gitInfo as any).remoteUrl || gitInfo.name || 'local';
221
+
222
+ const response = await fetch(`${API_URL}/api/scans/upload`, {
223
+ method: 'POST',
224
+ headers: {
225
+ 'Content-Type': 'application/json',
226
+ 'Authorization': `Bearer ${creds.token}`
227
+ },
228
+ body: JSON.stringify({
229
+ repositoryName: repoName,
230
+ repositoryUrl: repoUrl,
231
+ numericScore: deterministicScore.score,
232
+ score: deterministicScore.grade,
233
+ findings
234
+ })
235
+ });
236
+
237
+ syncStatus = response.ok ? 'SYNCED' : 'FAILED';
238
+ } catch {
239
+ syncStatus = 'FAILED';
240
+ }
241
+ }
242
+ }
243
+
244
+ // Transform scanner telemetry
245
+ const scannerTelemetryList: ScannerTelemetry[] = scannerResults.map(s => ({
246
+ scanner: s.scanner,
247
+ state: s.state,
248
+ durationMs: s.durationMs,
249
+ findingsCount: s.findings ? s.findings.length : 0,
250
+ reason: s.reason
251
+ }));
252
+
253
+ const activeDomainsCount = Object.values(coverageData).filter(Boolean).length;
254
+ const postureStatus = activeDomainsCount === 7 ? 'COMPLETE' : 'PARTIAL';
255
+
256
+ // 1. JSON Mode (Directive 17: Pure, machine-readable JSON)
257
+ if (isJson) {
258
+ const jsonOutput = generateJsonOutput({
259
+ deterministicScore,
260
+ findings,
261
+ coverageData,
262
+ activeDomainsCount,
263
+ postureStatus,
264
+ scanners: scannerTelemetryList,
265
+ gitInfo,
266
+ policyPassed,
267
+ failThreshold,
268
+ durationMs: elapsedMs
269
+ });
270
+
271
+ console.log(JSON.stringify(jsonOutput, null, 2));
272
+ process.exit(thresholdBreached ? 1 : (hasSystemError ? 2 : 0));
273
+ return;
274
+ }
275
+
276
+ // 2. CI Mode (Directive 18: Minimal, clean, automation-friendly)
277
+ if (options.ci) {
278
+ const ciLines = renderCIOutput({
279
+ deterministicScore,
280
+ findings,
281
+ policyPassed,
282
+ failThreshold,
283
+ scanners: scannerTelemetryList,
284
+ verbose: Boolean(options.verbose)
285
+ });
286
+ for (const line of ciLines) {
287
+ console.log(line);
288
+ }
289
+
290
+ process.exit(thresholdBreached ? 1 : (hasSystemError ? 2 : 0));
291
+ return;
292
+ }
293
+
294
+ // 3. Verbose Mode Diagnostics (Directive 19)
295
+ if (options.verbose) {
296
+ console.log(chalk.cyan('▶ VERBOSE DIAGNOSTICS'));
297
+ for (const sr of scannerTelemetryList) {
298
+ let hint = '';
299
+ if (sr.state === 'NOT_INSTALLED') {
300
+ if (sr.scanner === 'Semgrep') hint = ' (Install: pip install semgrep or brew install semgrep)';
301
+ else if (sr.scanner === 'Gitleaks') hint = ' (Install: brew install gitleaks or download from GitHub)';
302
+ else if (sr.scanner === 'Trivy') hint = ' (Install: brew install trivy or see aquasecurity.github.io)';
303
+ else if (sr.scanner === 'Checkov') hint = ' (Install: pip install checkov)';
304
+ }
305
+ console.log(` • ${sr.scanner.padEnd(12)} -> ${sr.state} in ${sr.durationMs || 0}ms (${sr.findingsCount} findings)${hint}`);
306
+ }
307
+ console.log('');
308
+ }
309
+
310
+ // 4. Interactive Terminal Dashboard
311
+ renderDashboard({
312
+ findings,
313
+ stats,
314
+ deterministicScore,
315
+ coverage: coverageData,
316
+ gitInfo,
317
+ duration,
318
+ scanners: scannerTelemetryList,
319
+ remediation: remediationData,
320
+ syncStatus,
321
+ policyThreshold: failThreshold,
322
+ verbose: Boolean(options.verbose)
323
+ });
324
+
325
+ if (hasSystemError) {
326
+ process.exit(2);
327
+ }
328
+
329
+ if (thresholdBreached) {
330
+ process.exit(1);
331
+ }
332
+
333
+ process.exit(0);
334
+ });
335
+
336
+ program
337
+ .command('login')
338
+ .description('Authenticate CLI with VibeGuard Cloud')
339
+ .option('-e, --email <email>', 'Account email')
340
+ .option('-p, --password <password>', 'Account password')
341
+ .option('--api-url <url>', 'VibeGuard API URL', process.env.VIBEGUARD_API_URL || 'https://vibeguard-eep3.onrender.com')
342
+ .action(async (options) => {
343
+ let email = options.email;
344
+ let password = options.password;
345
+ const apiUrl = options.apiUrl || process.env.VIBEGUARD_API_URL || 'https://vibeguard-eep3.onrender.com';
346
+
347
+ // Interactive prompt if flags not passed and TTY is active
348
+ if ((!email || !password) && process.stdin.isTTY) {
349
+ const rl = readline.createInterface({
350
+ input: process.stdin,
351
+ output: process.stdout
352
+ });
353
+
354
+ const question = (query: string) => new Promise<string>((res) => rl.question(query, res));
355
+
356
+ if (!email) {
357
+ email = await question(chalk.cyan('Enter VibeGuard Email: '));
358
+ }
359
+ if (!password) {
360
+ password = await question(chalk.cyan('Enter VibeGuard Password: '));
361
+ }
362
+ rl.close();
363
+ }
364
+
365
+ if (!email || !password) {
366
+ console.error(chalk.red('Error: Email and password are required. Use --email and --password flags.'));
367
+ process.exit(1);
368
+ }
369
+
370
+ const spinner = ora(chalk.cyan('Authenticating with VibeGuard Cloud...')).start();
371
+
372
+ try {
373
+ const response = await fetch(`${apiUrl}/api/auth/login`, {
374
+ method: 'POST',
375
+ headers: { 'Content-Type': 'application/json' },
376
+ body: JSON.stringify({ email: email.trim(), password })
377
+ });
378
+
379
+ const data = await response.json();
380
+
381
+ if (!response.ok) {
382
+ spinner.fail(chalk.red(`Authentication failed: ${data.error || 'Invalid credentials'}`));
383
+ process.exit(1);
384
+ }
385
+
386
+ saveCredentials({
387
+ token: data.token,
388
+ user: data.user,
389
+ apiUrl
390
+ });
391
+
392
+ spinner.succeed(chalk.green('Successfully authenticated with VibeGuard Cloud!'));
393
+ console.log('');
394
+ console.log(chalk.gray(' Account: ') + chalk.white.bold(data.user.email) + (data.user.name ? chalk.gray(` (${data.user.name})`) : ''));
395
+ console.log(chalk.gray(' API Host: ') + chalk.cyan(apiUrl));
396
+ console.log(chalk.gray(' Stored: ') + chalk.dim(getCredentialsPath()));
397
+ console.log('');
398
+ console.log(chalk.white('Cloud sync is now enabled. Run scans with ') + chalk.cyan('vibeguard scan --sync') + chalk.white(' to stream telemetry.'));
399
+ console.log('');
400
+ } catch (err: any) {
401
+ spinner.fail(chalk.red(`Connection error: Could not reach VibeGuard API at ${apiUrl}`));
402
+ console.error(chalk.dim(err.message || err));
403
+ process.exit(1);
404
+ }
405
+ });
406
+
407
+ program
408
+ .command('logout')
409
+ .description('Log out and remove local VibeGuard Cloud credentials')
410
+ .action(() => {
411
+ clearCredentials();
412
+ console.log(chalk.green('\n✓ Successfully logged out from VibeGuard Cloud.'));
413
+ console.log(chalk.gray(' Stored session cleared. Local scanning remains 100% operational.\n'));
414
+ });
415
+
416
+ program
417
+ .command('whoami')
418
+ .description('Display currently authenticated user and VibeGuard Cloud status')
419
+ .action(() => {
420
+ const creds = loadCredentials();
421
+ if (!creds || !creds.token) {
422
+ console.log(chalk.yellow('\n○ VibeGuard Cloud Status: NOT AUTHENTICATED'));
423
+ console.log(chalk.gray(" Run 'vibeguard login' to connect your CLI with VibeGuard Cloud.\n"));
424
+ return;
425
+ }
426
+
427
+ console.log(chalk.green('\n● VibeGuard Cloud Status: AUTHENTICATED'));
428
+ console.log(chalk.gray(' User: ') + chalk.white.bold(creds.user.email) + (creds.user.name ? chalk.gray(` (${creds.user.name})`) : ''));
429
+ console.log(chalk.gray(' API URL: ') + chalk.cyan(creds.apiUrl));
430
+ console.log(chalk.gray(' Saved: ') + chalk.dim(creds.savedAt || 'Unknown'));
431
+ console.log('');
432
+ });
433
+
434
+ const authCmd = program.command('auth').description('Manage VibeGuard Cloud authentication');
435
+
436
+ authCmd
437
+ .command('status')
438
+ .description('Display current VibeGuard Cloud authentication status')
439
+ .action(async () => {
440
+ const creds = loadCredentials();
441
+ if (!creds || !creds.token) {
442
+ console.log(chalk.yellow('\n○ VibeGuard Cloud Status: NOT AUTHENTICATED'));
443
+ console.log(chalk.gray(" Run 'vibeguard login' to connect your CLI with VibeGuard Cloud.\n"));
444
+ return;
445
+ }
446
+
447
+ console.log(chalk.green('\n● VibeGuard Cloud Status: AUTHENTICATED'));
448
+ console.log(chalk.gray(' User: ') + chalk.white.bold(creds.user.email) + (creds.user.name ? chalk.gray(` (${creds.user.name})`) : ''));
449
+ console.log(chalk.gray(' API URL: ') + chalk.cyan(creds.apiUrl));
450
+ console.log(chalk.gray(' Saved: ') + chalk.dim(creds.savedAt || 'Unknown'));
451
+ console.log('');
452
+ });
453
+
454
+ program.parse(process.argv);
455
+