Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ const customLanguageManager = new LanguageManager(undefined, [
| `--scan-file <file>` | File containing glob patterns to scan (one per line) | `null` |
| `--exclude-file <file>` | File containing glob patterns to exclude (one per line) | `null` |

## Exit Codes

The CLI uses different exit codes to indicate various conditions:

| Exit Code | Description |
|-----------|-------------|
| `0` | Success - no URLs found or URLs found without fail-on-error flag |
| `1` | URLs found when using the fail-on-error flag |
| `2` | Configuration error (e.g., invalid format, invalid concurrency value) |
| `3` | File read error (e.g., scan-file or exclude-file not found) |
| `4` | Parse error threshold exceeded (reserved for future use) |

These exit codes make it easier to distinguish between different types of failures in CI/CD pipelines and automation scripts.

## Supported Languages

| Language | Extensions | Tree-sitter Parser |
Expand Down
10 changes: 0 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

166 changes: 118 additions & 48 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ import { ConsoleLogger, NullLogger, ResultsOnlyLogger } from './logger';
import { OutputFormatter } from './outputFormatter';
const packageJson = require('../package.json');

/**
* Exit codes used by the CLI
*/
export const ExitCode = {
SUCCESS: 0, // Success, no URLs found
URLS_FOUND: 1, // URLs found (when --fail-on-error)
CONFIG_ERROR: 2, // Configuration error
FILE_READ_ERROR: 3, // File read error
PARSE_ERROR: 4, // Parse error threshold exceeded
} as const;

const program = new Command();

program
Expand All @@ -42,34 +53,78 @@ program
.option('--scan-file <file>', 'File containing glob patterns to scan (one per line)')
.option('--exclude-file <file>', 'File containing glob patterns to exclude (one per line)')
.action(async options => {
// Create appropriate logger based on CLI options
let logger;
if (options.quiet) {
logger = NullLogger;
} else if (options.resultsOnly) {
logger = ResultsOnlyLogger;
} else {
logger = ConsoleLogger;
const exitCode = await runCLI(options);
if (exitCode !== ExitCode.SUCCESS) {
process.exit(exitCode);
}
});

try {
// Create mutable copy of options for processing
let scanPatterns = (options.scan as string[]) || [];
let excludePatterns = (options.exclude as string[]) || [];
/**
* CLI options interface
*/
export interface CLIOptions {
scan?: string[];
exclude?: string[];
ignoreDomains?: string[];
includeComments?: boolean;
includeNonFqdn?: boolean;
format?: string;
output?: string | null;
resultsOnly?: boolean;
failOnError?: boolean;
concurrency?: number;
quiet?: boolean;
scanFile?: string;
excludeFile?: string;
}

// Load patterns from files if specified
if (options.scanFile) {
/**
* Main CLI logic extracted for testability
* Returns the exit code that should be used
*/
export async function runCLI(options: CLIOptions): Promise<number> {
// Create appropriate logger based on CLI options
let logger;
if (options.quiet) {
logger = NullLogger;
} else if (options.resultsOnly) {
logger = ResultsOnlyLogger;
} else {
logger = ConsoleLogger;
}

try {
// Create mutable copy of options for processing
let scanPatterns = (options.scan as string[]) || [];
let excludePatterns = (options.exclude as string[]) || [];

// Load patterns from files if specified
if (options.scanFile) {
try {
const fileScanPatterns = await loadPatternsFromFile(options.scanFile as string);
scanPatterns = [...scanPatterns, ...fileScanPatterns];
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Error loading scan patterns file:', errorMessage);
return ExitCode.FILE_READ_ERROR;
}
}

if (options.excludeFile) {
if (options.excludeFile) {
try {
const fileExcludePatterns = await loadPatternsFromFile(options.excludeFile as string);
excludePatterns = [...excludePatterns, ...fileExcludePatterns];
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Error loading exclude patterns file:', errorMessage);
return ExitCode.FILE_READ_ERROR;
}
}

// Create detector with options and logger
const detector = new URLDetector(
// Create detector with options and logger - catch configuration errors
let detector;
try {
detector = new URLDetector(
{
scan: scanPatterns,
exclude: excludePatterns,
Expand All @@ -84,45 +139,60 @@ program
},
logger,
);
} catch (error: unknown) {
// Configuration validation errors
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Configuration error:', errorMessage);
return ExitCode.CONFIG_ERROR;
}

// Process results
const results = await detector.process();
// Process results
const results = await detector.process();

// Calculate summary
const totalFiles = results.length;
const totalUrls = results.reduce((sum, r) => sum + r.urls.length, 0);
// Calculate summary
const totalFiles = results.length;
const totalUrls = results.reduce((sum, r) => sum + r.urls.length, 0);

// Handle output formatting - format results if we found URLs or if explicitly requested
if (totalUrls > 0) {
const outputFormatter = new OutputFormatter(
{
format: (options.format as OutputFormat) || 'table',
outputFile: (options.output as string) || null,
// Handle output formatting - format results if we found URLs or if explicitly requested
if (totalUrls > 0) {
const outputFormatter = new OutputFormatter(
{
format: (options.format as OutputFormat) || 'table',
outputFile: (options.output as string) || null,

withLineNumbers: true,
withFilenames: true,
context: 0,
},
logger,
);
withLineNumbers: true,
withFilenames: true,
context: 0,
},
logger,
);

await outputFormatter.formatAndOutput(results);
}
await outputFormatter.formatAndOutput(results);
}

// Print summary using logger
logger.info(`Processed ${totalFiles} file(s), found ${totalUrls} URL(s)`);
// Print summary using logger
logger.info(`Processed ${totalFiles} file(s), found ${totalUrls} URL(s)`);

// Exit with error code if URLs found and fail-on-error is set
if (options.failOnError && totalUrls > 0) {
process.exit(1);
}
} catch (error: unknown) {
// Use the same logger - errors will be shown in results-only mode, hidden in quiet mode
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Error:', errorMessage);
process.exit(1);
// Exit with error code if URLs found and fail-on-error is set
if (options.failOnError && totalUrls > 0) {
return ExitCode.URLS_FOUND;
}
});

return ExitCode.SUCCESS;
} catch (error: unknown) {
// Handle unexpected errors - check if it's a file read error
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Error:', errorMessage);

// Determine exit code based on error type
if (errorMessage.includes('Failed to find files') || errorMessage.includes('ENOENT')) {
return ExitCode.FILE_READ_ERROR;
} else {
// Default to config error for other unexpected errors
return ExitCode.CONFIG_ERROR;
}
}
}

async function loadPatternsFromFile(filePath: string): Promise<string[]> {
const content = await fs.promises.readFile(filePath, 'utf8');
Expand Down
Loading