@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/dist/credentials.js +63 -0
- package/dist/formatter.js +365 -121
- package/dist/index.js +317 -107
- package/jest.config.js +5 -0
- package/package.json +7 -3
- package/src/credentials.ts +74 -0
- package/src/formatter.ts +446 -143
- package/src/index.ts +455 -211
- package/tests/auth.test.ts +96 -0
- package/tests/formatter.test.ts +666 -0
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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
|