codeprobe-scanner 1.0.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.
Files changed (96) hide show
  1. package/.claude/settings.local.json +19 -0
  2. package/.dockerignore +17 -0
  3. package/.env.development +8 -0
  4. package/.env.example +20 -0
  5. package/.env.setup +214 -0
  6. package/.github/workflows/codeprobe-scan.yml +137 -0
  7. package/.github/workflows/codeprobe.yml +84 -0
  8. package/.github/workflows/scan-schedule.yml +28 -0
  9. package/ANALYSIS_SUMMARY.md +365 -0
  10. package/API_INTEGRATIONS.md +469 -0
  11. package/BUILD_PLAYBOOK.md +349 -0
  12. package/CLAUDE.md +106 -0
  13. package/DEPLOY.md +452 -0
  14. package/DEPLOYMENT_STATUS.md +240 -0
  15. package/DEPLOY_CHECKLIST.md +316 -0
  16. package/Dockerfile +24 -0
  17. package/EXECUTION_PLAN.html +1086 -0
  18. package/IMPLEMENTATION_COMPLETE.md +288 -0
  19. package/IMPLEMENTATION_SUMMARY.md +443 -0
  20. package/INTERACTIVE_FIX_FLOW.md +308 -0
  21. package/MIGRATION_COMPLETE.md +327 -0
  22. package/ORCHESTRATOR_SYNTHESIS.json +80 -0
  23. package/PENDING_WORK.md +308 -0
  24. package/PREFLIGHT_PLAN.md +182 -0
  25. package/QUICKSTART.md +305 -0
  26. package/README.md +15 -0
  27. package/STAGE_1_SETUP_ENGINE.md +245 -0
  28. package/STAGE_2_ARCHITECTURE.md +714 -0
  29. package/STAGE_2_CLI_VERIFICATION.md +269 -0
  30. package/STAGE_2_COMPLETE.md +332 -0
  31. package/STAGE_2_IMPLEMENTATION_PLAN.md +679 -0
  32. package/STAGE_3_COMPLETE.md +246 -0
  33. package/STAGE_3_DASHBOARD_POLISH.md +371 -0
  34. package/STAGE_3_SETUP.md +155 -0
  35. package/VIDEODB_INTEGRATION.md +237 -0
  36. package/archived/DASHBOARD_UI_WALKTHROUGH.md +392 -0
  37. package/archived/FRONTEND_SETUP.md +236 -0
  38. package/archived/auth.ts +40 -0
  39. package/archived/dashboard/components/BusinessImpactCard.tsx +48 -0
  40. package/archived/dashboard/components/CVETable.tsx +104 -0
  41. package/archived/dashboard/components/ErrorBoundary.tsx +48 -0
  42. package/archived/dashboard/components/PatchDiffViewer.tsx +43 -0
  43. package/archived/dashboard/components/RiskGauge.tsx +64 -0
  44. package/archived/dashboard/frontend.tsx +104 -0
  45. package/archived/dashboard/hooks/useAuth.ts +32 -0
  46. package/archived/dashboard/hooks/useScan.ts +65 -0
  47. package/archived/dashboard/index.html +15 -0
  48. package/archived/dashboard/pages/LoginPage.tsx +28 -0
  49. package/archived/dashboard/pages/ScanDetailPage.tsx +143 -0
  50. package/archived/dashboard/pages/ScansListPage.tsx +160 -0
  51. package/bin/install-and-run.sh +91 -0
  52. package/bun.lock +603 -0
  53. package/codeprobe-prd.md +674 -0
  54. package/cve-cache.json +25 -0
  55. package/demo-vulnerable-app/.github/workflows/codeprobe.yml +32 -0
  56. package/demo-vulnerable-app/README.md +70 -0
  57. package/demo-vulnerable-app/package-lock.json +27 -0
  58. package/demo-vulnerable-app/package.json +15 -0
  59. package/demo-vulnerable-app/server.js +34 -0
  60. package/demo.sh +45 -0
  61. package/index.ts +19 -0
  62. package/package.json +28 -0
  63. package/patches.json +12 -0
  64. package/serve-dashboard.ts +23 -0
  65. package/src/api/server-cli.ts +270 -0
  66. package/src/api/server.ts +293 -0
  67. package/src/bot/server.ts +113 -0
  68. package/src/cli/commands/report.ts +92 -0
  69. package/src/cli/commands/scan-with-fix.ts +123 -0
  70. package/src/cli/commands/scan.ts +137 -0
  71. package/src/cli/config.ts +188 -0
  72. package/src/cli/errors.ts +120 -0
  73. package/src/cli/index.ts +137 -0
  74. package/src/cli/progress.ts +119 -0
  75. package/src/cli-server.ts +523 -0
  76. package/src/engine/index.ts +90 -0
  77. package/src/engine/matcher.ts +115 -0
  78. package/src/engine/parser.ts +91 -0
  79. package/src/engine/patcher.ts +280 -0
  80. package/src/engine/report.ts +137 -0
  81. package/src/engine/sandbox.ts +222 -0
  82. package/src/engine/scraper.ts +122 -0
  83. package/src/integrations/videodb.ts +153 -0
  84. package/src/mcp/server.ts +149 -0
  85. package/src/scraper-cron.ts +103 -0
  86. package/src/shared/constants.ts +88 -0
  87. package/src/shared/types.ts +123 -0
  88. package/src/shared/utils.ts +80 -0
  89. package/src/test/cli.test.ts +211 -0
  90. package/src/test/dashboard.test.ts +38 -0
  91. package/src/test/demo-scan.json +32 -0
  92. package/src/test/engine.test.ts +157 -0
  93. package/tailwind.config.js +11 -0
  94. package/tsconfig.json +30 -0
  95. package/verify-dashboard.ts +87 -0
  96. package/verify-env.sh +98 -0
@@ -0,0 +1,137 @@
1
+ import chalk from 'chalk';
2
+ import { writeFile, chmod, mkdir } from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+ import path from 'path';
5
+ import { PATHS, EXIT_CODES, FILE_PERMISSIONS } from '../../shared/constants.js';
6
+ import { ProgressLogger, createEventHandler } from '../progress.js';
7
+ import { handleError, CodeProbeError } from '../errors.js';
8
+ import { generateScanId, formatRiskScore, msToHuman } from '../../shared/utils.js';
9
+ import { Report } from '../../shared/types.js';
10
+ import { createEngine } from '../../engine/index.js';
11
+
12
+ interface ScanOptions {
13
+ fix: boolean;
14
+ json: boolean;
15
+ verbose: boolean;
16
+ }
17
+
18
+ function parseArgs(args: string[]): { repoPath: string; options: ScanOptions } {
19
+ const options: ScanOptions = {
20
+ fix: false,
21
+ json: false,
22
+ verbose: false,
23
+ };
24
+
25
+ let repoPath = '.';
26
+
27
+ for (let i = 0; i < args.length; i++) {
28
+ const arg = args[i];
29
+ if (arg === '--fix') {
30
+ options.fix = true;
31
+ } else if (arg === '--json') {
32
+ options.json = true;
33
+ } else if (arg === '--verbose') {
34
+ options.verbose = true;
35
+ } else if (!arg.startsWith('--')) {
36
+ repoPath = arg;
37
+ }
38
+ }
39
+
40
+ return { repoPath, options };
41
+ }
42
+
43
+ async function saveReport(report: Report): Promise<string> {
44
+ // Ensure directory exists
45
+ if (!existsSync(PATHS.SCANS_DIR)) {
46
+ await mkdir(PATHS.SCANS_DIR, { mode: FILE_PERMISSIONS.DIR, recursive: true });
47
+ }
48
+
49
+ const scanPath = path.join(PATHS.SCANS_DIR, `${report.scan.id}.json`);
50
+ const content = JSON.stringify(report, null, 2);
51
+
52
+ await writeFile(scanPath, content, 'utf-8');
53
+ await chmod(scanPath, FILE_PERMISSIONS.FILE); // Owner read/write only
54
+
55
+ // Also update latest.json (copy, not symlink, for portability)
56
+ const latestPath = path.join(PATHS.SCANS_DIR, 'latest.json');
57
+ await writeFile(latestPath, content, 'utf-8');
58
+ await chmod(latestPath, FILE_PERMISSIONS.FILE);
59
+
60
+ return scanPath;
61
+ }
62
+
63
+ function displayReport(report: Report, json: boolean, durationMs: number): void {
64
+ if (json) {
65
+ console.log(JSON.stringify({ report, duration_ms: durationMs }, null, 2));
66
+ return;
67
+ }
68
+
69
+ const logger = new ProgressLogger(false);
70
+ logger.printSeparator();
71
+
72
+ console.log(chalk.bold('SCAN COMPLETE'));
73
+ console.log(`Risk Score: ${formatRiskScore(report.scan.risk_score)}`);
74
+ console.log(
75
+ chalk.cyan(
76
+ `Confirmed Exploitable: ${report.summary.exploitable_count} | ` +
77
+ `Theoretical Risk: ${report.summary.theoretical_count}`
78
+ )
79
+ );
80
+ console.log(`Patches Available: ${report.scan.patches_available}`);
81
+ console.log(`Duration: ${msToHuman(durationMs)}`);
82
+
83
+ if (report.scan.cves.length > 0) {
84
+ console.log(chalk.bold('\nCVE Details:'));
85
+ report.scan.cves.forEach((cve) => {
86
+ const exploitStatus = cve.exploitable
87
+ ? chalk.red('✓ CONFIRMED EXPLOITABLE')
88
+ : chalk.yellow('~ Theoretical Risk');
89
+ console.log(
90
+ ` ${cve.id}: ${cve.package} ${cve.version_vulnerable} [${cve.severity}] ${exploitStatus}`
91
+ );
92
+ if (cve.patch_version) {
93
+ console.log(` → Patch available: ${cve.patch_version}`);
94
+ }
95
+ });
96
+ }
97
+
98
+ logger.printSeparator();
99
+ }
100
+
101
+ export async function scanCommand(args: string[]): Promise<void> {
102
+ const { repoPath, options } = parseArgs(args);
103
+ const logger = new ProgressLogger(options.verbose);
104
+
105
+ logger.printHeader();
106
+
107
+ const startTime = Date.now();
108
+
109
+ try {
110
+ // Initialize engine
111
+ const engine = createEngine();
112
+
113
+ // Run the actual engine scan (not mocked)
114
+ const report = await engine.scan(repoPath);
115
+
116
+ const duration = Date.now() - startTime;
117
+
118
+ // Save report
119
+ const scanPath = await saveReport(report);
120
+ logger.logPhaseComplete('report', `Report saved to ${scanPath}`);
121
+
122
+ // Display results
123
+ displayReport(report, options.json, duration);
124
+
125
+ // If --fix, handle that next
126
+ if (options.fix) {
127
+ const { scanWithFixCommand } = await import('./scan-with-fix.js');
128
+ await scanWithFixCommand([repoPath], report, logger);
129
+ }
130
+
131
+ // Exit with appropriate code
132
+ const hasVulnerabilities = report.scan.cves.length > 0;
133
+ process.exit(hasVulnerabilities ? EXIT_CODES.VULNERABILITIES_FOUND : EXIT_CODES.SUCCESS);
134
+ } catch (error) {
135
+ handleError(error, logger, true);
136
+ }
137
+ }
@@ -0,0 +1,188 @@
1
+ import { existsSync, mkdirSync } from 'fs';
2
+ import { readFile, writeFile, chmod } from 'fs/promises';
3
+ import path from 'path';
4
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
5
+ import chalk from 'chalk';
6
+ import { PATHS, FILE_PERMISSIONS } from '../shared/constants.js';
7
+
8
+ interface ConfigData {
9
+ github_token?: string;
10
+ bright_data_api_key?: string;
11
+ daytona_api_key?: string;
12
+ nosana_api_key?: string;
13
+ [key: string]: string | undefined;
14
+ }
15
+
16
+ // Key derivation for AES-256-GCM
17
+ // Recommendation: Use machine ID for cross-session consistency
18
+ // For MVP: Use a fixed salt (in production, store per-machine)
19
+ const ENCRYPTION_SALT = 'codeprobe-mvp-salt-2026';
20
+ const ALGORITHM = 'aes-256-gcm';
21
+ const KEY_LENGTH = 32; // 256 bits
22
+ const IV_LENGTH = 16; // 128 bits for GCM
23
+ const TAG_LENGTH = 16; // GCM tag length
24
+
25
+ function getMachineKey(): Buffer {
26
+ // Derive key from stable machine fingerprint (hostname + platform + arch)
27
+ // Using process.pid breaks cross-session decryption
28
+ const os = require('os');
29
+ const fingerprint = `${os.hostname()}-${process.platform}-${process.arch}`;
30
+ return scryptSync(fingerprint, ENCRYPTION_SALT, KEY_LENGTH);
31
+ }
32
+
33
+ function encryptToken(token: string): string {
34
+ try {
35
+ const key = getMachineKey();
36
+ const iv = randomBytes(IV_LENGTH);
37
+ const cipher = createCipheriv(ALGORITHM, key, iv);
38
+
39
+ let encrypted = cipher.update(token, 'utf8', 'hex');
40
+ encrypted += cipher.final('hex');
41
+ const tag = cipher.getAuthTag();
42
+
43
+ // Format: iv:tag:encrypted
44
+ return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted}`;
45
+ } catch (error) {
46
+ console.warn(
47
+ chalk.yellow(
48
+ '⚠️ Token encryption failed. Falling back to plaintext (not secure).\n' +
49
+ 'Warning: ~/.codeprobe/config.json contains unencrypted secrets.'
50
+ )
51
+ );
52
+ return token;
53
+ }
54
+ }
55
+
56
+ function decryptToken(encryptedToken: string): string {
57
+ try {
58
+ // Check if token is already plaintext (fallback case)
59
+ if (!encryptedToken.includes(':')) {
60
+ return encryptedToken;
61
+ }
62
+
63
+ const [ivHex, tagHex, encrypted] = encryptedToken.split(':');
64
+ const key = getMachineKey();
65
+ const iv = Buffer.from(ivHex, 'hex');
66
+ const tag = Buffer.from(tagHex, 'hex');
67
+
68
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
69
+ decipher.setAuthTag(tag);
70
+
71
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
72
+ decrypted += decipher.final('utf8');
73
+
74
+ return decrypted;
75
+ } catch (error) {
76
+ console.warn(chalk.yellow('⚠️ Token decryption failed. Token may be corrupted.'));
77
+ return encryptedToken;
78
+ }
79
+ }
80
+
81
+ export async function ensureConfigDir(): Promise<void> {
82
+ if (!existsSync(PATHS.CODEPROBE_DIR)) {
83
+ mkdirSync(PATHS.CODEPROBE_DIR, { mode: FILE_PERMISSIONS.DIR, recursive: true });
84
+ }
85
+ if (!existsSync(PATHS.SCANS_DIR)) {
86
+ mkdirSync(PATHS.SCANS_DIR, { mode: FILE_PERMISSIONS.DIR, recursive: true });
87
+ }
88
+
89
+ // Ensure permissions are correct
90
+ try {
91
+ await chmod(PATHS.CODEPROBE_DIR, FILE_PERMISSIONS.DIR);
92
+ await chmod(PATHS.SCANS_DIR, FILE_PERMISSIONS.DIR);
93
+ } catch (error) {
94
+ // Ignore permission errors on some filesystems
95
+ }
96
+ }
97
+
98
+ async function loadConfig(): Promise<ConfigData> {
99
+ try {
100
+ if (!existsSync(PATHS.CONFIG_FILE)) {
101
+ return {};
102
+ }
103
+
104
+ const content = await readFile(PATHS.CONFIG_FILE, 'utf-8');
105
+ return JSON.parse(content);
106
+ } catch (error) {
107
+ console.warn(chalk.yellow('⚠️ Failed to load config file.'));
108
+ return {};
109
+ }
110
+ }
111
+
112
+ async function saveConfig(config: ConfigData): Promise<void> {
113
+ await ensureConfigDir();
114
+
115
+ const content = JSON.stringify(config, null, 2);
116
+ await writeFile(PATHS.CONFIG_FILE, content, 'utf-8');
117
+ await chmod(PATHS.CONFIG_FILE, FILE_PERMISSIONS.FILE);
118
+ }
119
+
120
+ export async function getConfig(key?: string): Promise<ConfigData | string | undefined> {
121
+ const config = await loadConfig();
122
+
123
+ if (key) {
124
+ const value = config[key];
125
+ if (!value) return undefined;
126
+
127
+ // Decrypt if it looks like an encrypted token
128
+ if (typeof value === 'string' && value.includes(':')) {
129
+ return decryptToken(value);
130
+ }
131
+ return value;
132
+ }
133
+
134
+ return config;
135
+ }
136
+
137
+ export async function setConfig(key: string, value: string): Promise<void> {
138
+ const config = await loadConfig();
139
+
140
+ // Encrypt if it's a token/secret field (specific fields only)
141
+ const secretFields = ['github_token', 'bright_data_api_key', 'daytona_api_key', 'nosana_api_key'];
142
+ if (secretFields.includes(key.toLowerCase())) {
143
+ config[key] = encryptToken(value);
144
+ } else {
145
+ config[key] = value;
146
+ }
147
+
148
+ await saveConfig(config);
149
+ console.log(chalk.green(`✓ Config saved: ${key}`));
150
+ }
151
+
152
+ export async function clearConfig(key: string): Promise<void> {
153
+ const config = await loadConfig();
154
+ delete config[key];
155
+ await saveConfig(config);
156
+ console.log(chalk.green(`✓ Config cleared: ${key}`));
157
+ }
158
+
159
+ export async function getApiKey(service: 'BRIGHT_DATA' | 'DAYTONA' | 'NOSANA'): Promise<string> {
160
+ const envKey = process.env[`${service}_API_KEY`];
161
+ if (envKey) {
162
+ return envKey;
163
+ }
164
+
165
+ const configKey = await getConfig(service.toLowerCase() + '_api_key');
166
+ if (configKey && typeof configKey === 'string') {
167
+ return configKey;
168
+ }
169
+
170
+ throw new Error(
171
+ `No ${service} API key found.\n` +
172
+ `Set environment variable: export ${service}_API_KEY=<key>\n` +
173
+ `Or run: codeprobe config set ${service.toLowerCase()}_api_key <key>`
174
+ );
175
+ }
176
+
177
+ export async function getGitHubToken(): Promise<string> {
178
+ const token = await getConfig('github_token');
179
+ if (token && typeof token === 'string') {
180
+ return token;
181
+ }
182
+
183
+ throw new Error(
184
+ 'No GitHub token found.\n' +
185
+ 'Set: codeprobe config set github_token <token>\n' +
186
+ 'Or: export GITHUB_TOKEN=<token>'
187
+ );
188
+ }
@@ -0,0 +1,120 @@
1
+ import chalk from 'chalk';
2
+ import { ProgressLogger } from './progress.js';
3
+
4
+ export class CodeProbeError extends Error {
5
+ constructor(
6
+ public code: string,
7
+ public message: string,
8
+ public details?: string
9
+ ) {
10
+ super(message);
11
+ this.name = 'CodeProbeError';
12
+ }
13
+ }
14
+
15
+ export class BrightDataError extends CodeProbeError {
16
+ constructor(details?: string) {
17
+ super('BRIGHT_DATA_FAILED', 'CVE scraping failed', details);
18
+ }
19
+ }
20
+
21
+ export class DaytonaError extends CodeProbeError {
22
+ constructor(details?: string) {
23
+ super('DAYTONA_FAILED', 'Sandbox operation failed', details);
24
+ }
25
+ }
26
+
27
+ export class NetworkError extends CodeProbeError {
28
+ constructor(details?: string) {
29
+ super('NETWORK_ERROR', 'Network operation failed', details);
30
+ }
31
+ }
32
+
33
+ export class GitError extends CodeProbeError {
34
+ constructor(details?: string) {
35
+ super('GIT_ERROR', 'Git operation failed', details);
36
+ }
37
+ }
38
+
39
+ export class ConfigError extends CodeProbeError {
40
+ constructor(details?: string) {
41
+ super('CONFIG_ERROR', 'Configuration error', details);
42
+ }
43
+ }
44
+
45
+ export function handleError(error: unknown, logger: ProgressLogger, exitOnError = true): void {
46
+ console.log('');
47
+ logger.printSeparator();
48
+
49
+ if (error instanceof CodeProbeError) {
50
+ logger.logError(error.message, error.details);
51
+
52
+ // Provide guidance for specific errors
53
+ if (error.code === 'BRIGHT_DATA_FAILED') {
54
+ console.log(chalk.yellow('→ Using cached CVE data (may be outdated)'));
55
+ console.log(chalk.cyan('→ Run: codeprobe config set bright_data_api_key <key>'));
56
+ } else if (error.code === 'GIT_ERROR') {
57
+ console.log(chalk.yellow('→ Ensure git repository is clean'));
58
+ console.log(chalk.cyan('→ Run: git status && git commit'));
59
+ } else if (error.code === 'CONFIG_ERROR') {
60
+ console.log(chalk.yellow('→ Run: codeprobe config set <key> <value>'));
61
+ }
62
+ } else if (error instanceof Error) {
63
+ logger.logError(error.message, error.stack);
64
+ } else {
65
+ logger.logError('Unknown error occurred');
66
+ }
67
+
68
+ logger.printSeparator();
69
+ console.log('');
70
+
71
+ if (exitOnError) {
72
+ process.exit(2); // Exit code 2 = scan failed
73
+ }
74
+ }
75
+
76
+ export function wrapWithTimeout<T>(
77
+ promise: Promise<T>,
78
+ timeoutMs: number,
79
+ errorMessage: string
80
+ ): Promise<T> {
81
+ return Promise.race([
82
+ promise,
83
+ new Promise<T>((_, reject) =>
84
+ setTimeout(() => reject(new Error(`${errorMessage} (timeout: ${timeoutMs}ms)`)), timeoutMs)
85
+ ),
86
+ ]);
87
+ }
88
+
89
+ export async function retryWithBackoff<T>(
90
+ fn: () => Promise<T>,
91
+ maxRetries = 2,
92
+ backoffMs = 1000
93
+ ): Promise<T> {
94
+ let lastError: Error | undefined;
95
+
96
+ for (let i = 0; i <= maxRetries; i++) {
97
+ try {
98
+ return await fn();
99
+ } catch (error) {
100
+ lastError = error instanceof Error ? error : new Error(String(error));
101
+ if (i < maxRetries) {
102
+ await new Promise((resolve) => setTimeout(resolve, backoffMs * (i + 1)));
103
+ }
104
+ }
105
+ }
106
+
107
+ throw lastError;
108
+ }
109
+
110
+ export function formatError(error: unknown): string {
111
+ if (error instanceof Error) {
112
+ return error.message;
113
+ }
114
+ return String(error);
115
+ }
116
+
117
+ export function logFallback(message: string, reason: string): void {
118
+ console.log(chalk.yellow(`→ ${message}`));
119
+ console.log(chalk.gray(` Reason: ${reason}`));
120
+ }
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import chalk from 'chalk';
4
+ import { APP_NAME, APP_VERSION, EXIT_CODES } from '../shared/constants.js';
5
+ import { ProgressLogger } from './progress.js';
6
+ import { handleError } from './errors.js';
7
+ import { scanCommand } from './commands/scan.js';
8
+ import { reportCommand } from './commands/report.js';
9
+
10
+ const logger = new ProgressLogger();
11
+
12
+ function showHelp(): void {
13
+ console.log(`
14
+ ${chalk.bold.cyan(`⚡ ${APP_NAME} v${APP_VERSION}`)}
15
+
16
+ ${chalk.bold('USAGE')}
17
+ codeprobe <command> [options] [arguments]
18
+
19
+ ${chalk.bold('COMMANDS')}
20
+ scan [path] Scan a repository for vulnerabilities (default: current dir)
21
+ report Display last scan results
22
+ config Manage configuration
23
+ help Show this help message
24
+
25
+ ${chalk.bold('OPTIONS')}
26
+ --fix Auto-fix vulnerabilities (creates git branch & commits)
27
+ --json Output results as JSON
28
+ --verbose Show detailed logs
29
+ --help Show help
30
+
31
+ ${chalk.bold('EXAMPLES')}
32
+ codeprobe scan
33
+ codeprobe scan ./my-app
34
+ codeprobe scan --fix
35
+ codeprobe scan --json > report.json
36
+ codeprobe report
37
+ codeprobe config set bright_data_api_key <key>
38
+
39
+ ${chalk.bold('DOCS')}
40
+ https://github.com/codeprobe/codeprobe
41
+ `);
42
+ }
43
+
44
+ async function main(): Promise<void> {
45
+ const args = process.argv.slice(2);
46
+
47
+ // No args = show help
48
+ if (args.length === 0) {
49
+ showHelp();
50
+ process.exit(EXIT_CODES.SUCCESS);
51
+ }
52
+
53
+ const command = args[0];
54
+ const restArgs = args.slice(1);
55
+
56
+ try {
57
+ switch (command) {
58
+ case 'scan':
59
+ await scanCommand(restArgs);
60
+ break;
61
+
62
+ case 'report':
63
+ await reportCommand(restArgs);
64
+ break;
65
+
66
+ case 'config':
67
+ await handleConfigCommand(restArgs);
68
+ break;
69
+
70
+ case '--help':
71
+ case '-h':
72
+ case 'help':
73
+ showHelp();
74
+ break;
75
+
76
+ default:
77
+ console.error(chalk.red(`Unknown command: ${command}`));
78
+ console.log(`Run ${chalk.cyan('codeprobe --help')} for usage`);
79
+ process.exit(EXIT_CODES.SCAN_FAILED);
80
+ }
81
+ } catch (error) {
82
+ handleError(error, logger, true);
83
+ }
84
+ }
85
+
86
+ async function handleConfigCommand(args: string[]): Promise<void> {
87
+ const { getConfig, setConfig, clearConfig } = await import('./config.js');
88
+
89
+ const subcommand = args[0];
90
+
91
+ switch (subcommand) {
92
+ case 'get':
93
+ {
94
+ const key = args[1];
95
+ if (!key) {
96
+ const config = await getConfig();
97
+ console.log(JSON.stringify(config, null, 2));
98
+ } else {
99
+ const value = await getConfig(key);
100
+ console.log(value || `${key} not set`);
101
+ }
102
+ }
103
+ break;
104
+
105
+ case 'set':
106
+ {
107
+ const key = args[1];
108
+ const value = args[2];
109
+ if (!key || !value) {
110
+ console.error(chalk.red('Usage: codeprobe config set <key> <value>'));
111
+ process.exit(EXIT_CODES.SCAN_FAILED);
112
+ }
113
+ await setConfig(key, value);
114
+ }
115
+ break;
116
+
117
+ case 'clear':
118
+ {
119
+ const key = args[1];
120
+ if (!key) {
121
+ console.error(chalk.red('Usage: codeprobe config clear <key>'));
122
+ process.exit(EXIT_CODES.SCAN_FAILED);
123
+ }
124
+ await clearConfig(key);
125
+ }
126
+ break;
127
+
128
+ default:
129
+ console.error(chalk.red(`Unknown config subcommand: ${subcommand}`));
130
+ console.log(`Usage: codeprobe config [get|set|clear]`);
131
+ process.exit(EXIT_CODES.SCAN_FAILED);
132
+ }
133
+ }
134
+
135
+ main().catch((error) => {
136
+ handleError(error, logger, true);
137
+ });
@@ -0,0 +1,119 @@
1
+ import chalk from 'chalk';
2
+ import dayjs from 'dayjs';
3
+ import { ScanEvent } from '../shared/types.js';
4
+
5
+ export class ProgressLogger {
6
+ private startTime: number = Date.now();
7
+ private lastPhase: string = '';
8
+
9
+ constructor(private verbose: boolean = false) {}
10
+
11
+ log(event: ScanEvent): void {
12
+ if (!this.verbose && event.level === 'info') {
13
+ // Only show progress/complete/error in non-verbose mode
14
+ if (event.status === 'progress' || event.status === 'start') {
15
+ return;
16
+ }
17
+ }
18
+
19
+ const timestamp = dayjs().format('HH:mm:ss');
20
+ const prefix = `[${timestamp}]`;
21
+
22
+ let output = '';
23
+ const icon = this.getIcon(event.level, event.status);
24
+
25
+ switch (event.level) {
26
+ case 'success':
27
+ output = chalk.green(`${prefix} ${icon} ${event.message}`);
28
+ break;
29
+ case 'warn':
30
+ output = chalk.yellow(`${prefix} ${icon} ${event.message}`);
31
+ break;
32
+ case 'error':
33
+ output = chalk.red(`${prefix} ${icon} ${event.message}`);
34
+ break;
35
+ default:
36
+ output = chalk.cyan(`${prefix} ${icon} ${event.message}`);
37
+ }
38
+
39
+ if (event.metadata && this.verbose) {
40
+ output += chalk.gray(` ${JSON.stringify(event.metadata)}`);
41
+ }
42
+
43
+ console.log(output);
44
+ this.lastPhase = event.phase;
45
+ }
46
+
47
+ private getIcon(level: string, status: string): string {
48
+ switch (level) {
49
+ case 'success':
50
+ return '✓';
51
+ case 'error':
52
+ return '❌';
53
+ case 'warn':
54
+ return '⚠️';
55
+ default:
56
+ if (status === 'start') return '▶️';
57
+ if (status === 'complete') return '✓';
58
+ return '•';
59
+ }
60
+ }
61
+
62
+ logPhaseStart(phase: string, message: string): void {
63
+ const timestamp = dayjs().format('HH:mm:ss');
64
+ console.log(chalk.cyan(`[${timestamp}] ▶️ ${message}...`));
65
+ }
66
+
67
+ logPhaseComplete(phase: string, message: string): void {
68
+ const timestamp = dayjs().format('HH:mm:ss');
69
+ console.log(chalk.green(`[${timestamp}] ✓ ${message}`));
70
+ }
71
+
72
+ logError(message: string, details?: string): void {
73
+ const timestamp = dayjs().format('HH:mm:ss');
74
+ console.log(chalk.red(`[${timestamp}] ❌ ${message}`));
75
+ if (details) {
76
+ console.log(chalk.gray(` ${details}`));
77
+ }
78
+ }
79
+
80
+ logWarning(message: string, details?: string): void {
81
+ const timestamp = dayjs().format('HH:mm:ss');
82
+ console.log(chalk.yellow(`[${timestamp}] ⚠️ ${message}`));
83
+ if (details) {
84
+ console.log(chalk.gray(` ${details}`));
85
+ }
86
+ }
87
+
88
+ printSeparator(): void {
89
+ console.log(chalk.gray('────────────────────────────────────────────────'));
90
+ }
91
+
92
+ printHeader(): void {
93
+ console.log(chalk.bold.cyan('⚡ CodeProbe v1.0.0'));
94
+ }
95
+
96
+ printElapsedTime(): void {
97
+ const elapsed = Date.now() - this.startTime;
98
+ const ms = elapsed % 1000;
99
+ const s = Math.floor(elapsed / 1000) % 60;
100
+ const m = Math.floor(elapsed / 60000);
101
+
102
+ const timeStr =
103
+ m > 0
104
+ ? `${m}m ${s}s`
105
+ : s > 0
106
+ ? `${s}s`
107
+ : `${ms}ms`;
108
+
109
+ console.log(chalk.gray(`⏱️ Completed in ${timeStr}`));
110
+ }
111
+ }
112
+
113
+ export function createEventHandler(verbose: boolean = false) {
114
+ const logger = new ProgressLogger(verbose);
115
+
116
+ return (event: ScanEvent) => {
117
+ logger.log(event);
118
+ };
119
+ }