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