Skip to content
Merged
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ pnpm build
# Display all environment variables
./dist/cli.js

# Filter by prefix (case-insensitive)
# Prefix filtering (case-insensitive)
./dist/cli.js VITE_
./dist/cli.js NODE_
./dist/cli.js PATH

# Filter by partial match (case-insensitive)
./dist/cli.js --filter API
./dist/cli.js --filter database

# Display help
./dist/cli.js --help
Expand Down
13 changes: 5 additions & 8 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node

import { parseProcessArgs } from './cli/parser/index.js';
import type { ParsedArgs } from './cli/parser/types.js';
import { handleParseError, handleSystemError } from './cli/errors/index.js';
import { createCliConfig } from './cli/config.js';
import {
Expand All @@ -12,18 +13,14 @@ import {
/**
* Execute command based on parsed arguments
*/
function executeCommand(
command: string,
args: ReadonlyArray<string>,
config: ReturnType<typeof createCliConfig>,
): void {
function executeCommand(parsedArgs: ParsedArgs, config: ReturnType<typeof createCliConfig>): void {
const commandHandlers: Record<string, () => void> = {
help: () => handleHelpCommand(config.helpText),
version: () => handleVersionCommand(),
main: () => handleMainCommand(args),
main: () => handleMainCommand(parsedArgs.arguments, parsedArgs),
};

const handler = commandHandlers[command];
const handler = commandHandlers[parsedArgs.command];
if (handler) {
handler();
} else {
Expand All @@ -45,7 +42,7 @@ function main(): void {
return;
}

executeCommand(parseResult.data.command, parseResult.data.arguments, config);
executeCommand(parseResult.data, config);
} catch (error) {
handleSystemError(error);
}
Expand Down
7 changes: 4 additions & 3 deletions src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function createCliConfig(): CliConfig {
version: pkg.version,
description: 'Beautiful environment variable viewer',
helpText: formatHelpText(),
supportedOptions: ['help', 'version'],
supportedOptions: ['help', 'version', 'filter'],
};
}

Expand All @@ -28,8 +28,9 @@ function formatHelpText(): string {
' envui [options]',
'',
'Options:',
' -h, --help display help for command',
' -v, --version display version number',
' -h, --help display help for command',
' -v, --version display version number',
' --filter TEXT filter variables containing TEXT (case-insensitive)',
'',
CLI_MESSAGES.HELP_EXAMPLES,
CLI_MESSAGES.HELP_DESCRIPTION,
Expand Down
18 changes: 11 additions & 7 deletions src/cli/errors/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ export function handleParseError(parseError: {
message: string;
code: number;
}): void {
const error = createCliError(
parseError.type === 'unknown_option'
? CliErrorType.UNKNOWN_OPTION
: CliErrorType.INVALID_ARGUMENT,
parseError.message,
parseError.code,
);
let errorType: CliErrorType;

if (parseError.type === 'unknown_option') {
errorType = CliErrorType.UNKNOWN_OPTION;
} else if (parseError.type === 'filter_requires_value') {
errorType = CliErrorType.INVALID_ARGUMENT;
} else {
errorType = CliErrorType.INVALID_ARGUMENT;
}

const error = createCliError(errorType, parseError.message, parseError.code);
handleCliError(error);
process.exit(error.exitCode);
}
Expand Down
35 changes: 28 additions & 7 deletions src/cli/errors/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ import { logger } from '../../utils/logger.js';
export function handleCliError(error: CliErrorObject): void {
switch (error.type) {
case CliErrorType.UNKNOWN_OPTION:
logger.error(error.message);
logger.error(CLI_MESSAGES.INVALID_OPTION_HELP);
logger.userError(error.message, { hint: CLI_MESSAGES.INVALID_OPTION_HELP });
break;

case CliErrorType.INVALID_ARGUMENT:
logger.error(error.message);
logger.error(CLI_MESSAGES.INVALID_OPTION_HELP);
// For --filter errors, show detailed usage; for others, show hint
if (error.message.includes('--filter')) {
logger.userError(error.message, {
usage:
' envui [PREFIX] # Filter by prefix\n envui --filter TEXT # Filter by partial match',
});
} else {
logger.userError(error.message, { hint: CLI_MESSAGES.INVALID_OPTION_HELP });
}
break;

case CliErrorType.HELP_REQUESTED:
Expand All @@ -25,12 +31,27 @@ export function handleCliError(error: CliErrorObject): void {
// Version display handled in main logic
break;

case CliErrorType.FILTER_CONFLICT:
logger.userError(error.message, {
usage:
' envui [PREFIX] # Filter by prefix\n envui --filter TEXT # Filter by partial match',
});
break;

case CliErrorType.NO_DATA_FOUND:
logger.userInfo(error.message);
break;

case CliErrorType.SUCCESS_EXIT:
logger.userInfo(error.message);
break;

case CliErrorType.SYSTEM_ERROR:
logger.error(error.message);
logger.userError(error.message);
break;

default:
logger.error('An unexpected error occurred');
logger.userError('An unexpected error occurred');
}
}

Expand All @@ -40,7 +61,7 @@ export function handleCliError(error: CliErrorObject): void {
export function createUnknownOptionError(option: string): CliErrorObject {
return createCliError(
CliErrorType.UNKNOWN_OPTION,
`error: unknown option '${option}'`,
`unknown option '${option}'`,
EXIT_CODES.INVALID_ARGUMENT,
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/cli/errors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export enum CliErrorType {
HELP_REQUESTED = 'help_requested',
VERSION_REQUESTED = 'version_requested',
SYSTEM_ERROR = 'system_error',
FILTER_CONFLICT = 'filter_conflict',
NO_DATA_FOUND = 'no_data_found',
SUCCESS_EXIT = 'success_exit',
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/cli/handlers/helpHandler.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { EXIT_CODES } from '../../constants/index.js';
import { createCliError, CliErrorType } from '../errors/types.js';

/**
* Handle help command
*/
export function handleHelpCommand(helpText: string): void {
console.log(helpText);
process.exit(EXIT_CODES.SUCCESS);
throw createCliError(CliErrorType.SUCCESS_EXIT, helpText, EXIT_CODES.SUCCESS);
}
69 changes: 47 additions & 22 deletions src/cli/handlers/mainHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,87 @@ import { createEnvironmentTable } from '../../core/table.js';
import { readEnvironmentVariables } from '../../core/envReader.js';
import { filterEnvironmentVariables, generateNoMatchMessage } from '../../core/filter.js';
import type { FilterConfig } from '../../types/environment.js';
import type { ParsedArgs } from '../parser/types.js';
import { EXIT_CODES, ERROR_MESSAGES } from '../../constants/index.js';
import { logger } from '../../utils/logger.js';
import { createCliError, CliErrorType } from '../errors/types.js';

/**
* Create filter configuration from arguments
* Create filter configuration from parsed arguments
*/
function createFilterConfig(args?: ReadonlyArray<string>): FilterConfig {
if (!args || args.length === 0) {
return { type: 'none' };
function createFilterConfig(parsedArgs: ParsedArgs): FilterConfig {
// Check for --filter option
if (parsedArgs.filterValue) {
// Check for conflict with prefix filter
if (parsedArgs.arguments.length > 0) {
throw createCliError(
CliErrorType.FILTER_CONFLICT,
'cannot use prefix filter and --filter option together',
EXIT_CODES.INVALID_ARGUMENT,
);
}

return { type: 'partial', value: parsedArgs.filterValue };
}

// Use the first argument as prefix
const prefix = args[0];
if (!prefix || prefix.trim() === '') {
return { type: 'none' };
// Check for prefix filter (first argument)
if (parsedArgs.arguments.length > 0) {
const prefix = parsedArgs.arguments[0];
if (prefix && prefix.trim() !== '') {
return { type: 'prefix', value: prefix.trim() };
}
}

return { type: 'prefix', value: prefix };
return { type: 'none' };
}

/**
* Handle main command (display environment variables)
*/
export function handleMainCommand(args?: ReadonlyArray<string>): void {
export function handleMainCommand(args?: ReadonlyArray<string>, parsedArgs?: ParsedArgs): void {
// Get environment variables
const environmentData = readEnvironmentVariables();

// Handle case when no environment variables found
if (environmentData.length === 0) {
console.log(ERROR_MESSAGES.NO_ENVIRONMENT_VARIABLES);
process.exit(EXIT_CODES.DATA_NOT_FOUND);
throw createCliError(
CliErrorType.NO_DATA_FOUND,
ERROR_MESSAGES.NO_ENVIRONMENT_VARIABLES,
EXIT_CODES.DATA_NOT_FOUND,
);
}

// Create filter configuration
const filterConfig = createFilterConfig(args);
// For backward compatibility, create a minimal ParsedArgs if not provided
const effectiveParsedArgs = parsedArgs ?? {
command: 'main' as const,
options: [],
flags: new Set<string>(),
arguments: args ?? [],
errors: [],
};

const filterConfig = createFilterConfig(effectiveParsedArgs);

// Apply filter
const filterResult = filterEnvironmentVariables(environmentData, filterConfig);

// Handle no matches
if (filterResult.matchCount === 0) {
if (filterConfig.type === 'prefix') {
// Show filter info even when no matches
console.log(filterResult.filterInfo);
let message = generateNoMatchMessage(filterConfig);
if (filterConfig.type !== 'none' && filterResult.filterInfo) {
message = `${filterResult.filterInfo}\n${message}`;
}
console.log(generateNoMatchMessage(filterConfig));
process.exit(EXIT_CODES.DATA_NOT_FOUND);
throw createCliError(CliErrorType.NO_DATA_FOUND, message, EXIT_CODES.DATA_NOT_FOUND);
}

// Display filter info if filter is applied
if (filterResult.filterInfo) {
console.log(filterResult.filterInfo);
console.log(); // Empty line after filter info
logger.userInfo(filterResult.filterInfo);
logger.userInfo(''); // Empty line after filter info
}

// Display table
const table = createEnvironmentTable(filterResult.filtered);
console.log(table);
process.exit(EXIT_CODES.SUCCESS);
throw createCliError(CliErrorType.SUCCESS_EXIT, table, EXIT_CODES.SUCCESS);
}
4 changes: 2 additions & 2 deletions src/cli/handlers/versionHandler.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getVersionText } from '../config.js';
import { EXIT_CODES } from '../../constants/index.js';
import { createCliError, CliErrorType } from '../errors/types.js';

/**
* Handle version command
*/
export function handleVersionCommand(): void {
console.log(getVersionText());
process.exit(EXIT_CODES.SUCCESS);
throw createCliError(CliErrorType.SUCCESS_EXIT, getVersionText(), EXIT_CODES.SUCCESS);
}
42 changes: 41 additions & 1 deletion src/cli/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,58 @@ export function parseArgs(args: readonly string[], config: CliConfig): ParseResu
const flags = new Set<string>();
const parsedArguments: string[] = [];
let command: ParsedArgs['command'] = 'main';
let filterValue: string | undefined;

// Check help flag with priority
let hasHelp = false;
let hasVersion = false;
let skipNext = false;

for (let i = 0; i < tokens.length; i++) {
if (skipNext) {
skipNext = false;
continue;
}

const token = tokens[i];
if (!token) continue;

for (const token of tokens) {
if (token.type === 'option') {
const normalizedOption = OPTION_ALIASES[token.value] ?? token.value;

if (normalizedOption === 'help') {
hasHelp = true;
} else if (normalizedOption === 'version') {
hasVersion = true;
} else if (normalizedOption === 'filter') {
// Handle --filter option with value
const nextToken = tokens[i + 1];
if (!nextToken || nextToken.type !== 'argument') {
return {
success: false,
error: {
type: 'filter_requires_value',
message: '--filter option requires a search text',
code: 1,
},
};
}

// Check if the value is empty or whitespace only
const value = nextToken.value.trim();
if (value === '') {
return {
success: false,
error: {
type: 'filter_requires_value',
message: '--filter option requires a search text',
code: 1,
},
};
}

filterValue = value;
skipNext = true;
}

options.push(normalizedOption);
Expand All @@ -92,6 +131,7 @@ export function parseArgs(args: readonly string[], config: CliConfig): ParseResu
flags,
arguments: parsedArguments,
errors: [],
filterValue,
},
};
}
Expand Down
3 changes: 2 additions & 1 deletion src/cli/parser/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Type definition for CLI parsing error (for parser internal use)
*/
export interface ParseError {
readonly type: 'unknown_option' | 'invalid_argument' | 'system_error';
readonly type: 'unknown_option' | 'invalid_argument' | 'system_error' | 'filter_requires_value';
readonly message: string;
readonly code: number;
}
Expand All @@ -16,6 +16,7 @@ export interface ParsedArgs {
readonly flags: ReadonlySet<string>;
readonly arguments: ReadonlyArray<string>;
readonly errors: ReadonlyArray<ParseError>;
readonly filterValue?: string;
}

/**
Expand Down
Loading