Skip to content

Commit 169717e

Browse files
authored
Merge pull request #41 from sakamotopaya/story-16-comprehensive-error-handling
Story 16: Add Comprehensive Error Handling for CLI Utility
2 parents 12854ff + 4d31908 commit 169717e

20 files changed

+3914
-0
lines changed

src/cli/errors/CLIError.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Base CLI error class with comprehensive error handling features
3+
*/
4+
5+
import { ErrorCategory, ErrorSeverity, ErrorContext } from "../types/error-types"
6+
7+
export abstract class CLIError extends Error {
8+
abstract readonly category: ErrorCategory
9+
abstract readonly severity: ErrorSeverity
10+
abstract readonly isRecoverable: boolean
11+
12+
constructor(
13+
message: string,
14+
public readonly code: string,
15+
public readonly context?: ErrorContext,
16+
public override readonly cause?: Error,
17+
) {
18+
super(message)
19+
this.name = this.constructor.name
20+
21+
// Maintain proper stack trace
22+
if (Error.captureStackTrace) {
23+
Error.captureStackTrace(this, this.constructor)
24+
}
25+
}
26+
27+
abstract getSuggestedActions(): string[]
28+
abstract getDocumentationLinks(): string[]
29+
30+
/**
31+
* Get user-friendly error message
32+
*/
33+
getUserFriendlyMessage(): string {
34+
return this.message
35+
}
36+
37+
/**
38+
* Get technical details for debugging
39+
*/
40+
getTechnicalDetails(): Record<string, any> {
41+
return {
42+
code: this.code,
43+
category: this.category,
44+
severity: this.severity,
45+
isRecoverable: this.isRecoverable,
46+
cause: this.cause?.message,
47+
context: this.context,
48+
}
49+
}
50+
51+
/**
52+
* Convert error to JSON representation
53+
*/
54+
toJSON(): Record<string, any> {
55+
return {
56+
name: this.name,
57+
message: this.message,
58+
code: this.code,
59+
category: this.category,
60+
severity: this.severity,
61+
isRecoverable: this.isRecoverable,
62+
suggestedActions: this.getSuggestedActions(),
63+
documentationLinks: this.getDocumentationLinks(),
64+
stack: this.stack,
65+
cause: this.cause?.message,
66+
context: this.context,
67+
}
68+
}
69+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* Configuration related errors
3+
*/
4+
5+
import { ErrorCategory, ErrorSeverity, ErrorContext } from "../types/error-types"
6+
import { CLIError } from "./CLIError"
7+
8+
export class ConfigurationError extends CLIError {
9+
readonly category = ErrorCategory.CONFIGURATION
10+
readonly severity = ErrorSeverity.HIGH
11+
readonly isRecoverable = true
12+
13+
constructor(
14+
message: string,
15+
code: string,
16+
public readonly configPath?: string,
17+
public readonly configKey?: string,
18+
context?: ErrorContext,
19+
cause?: Error,
20+
) {
21+
super(message, code, context, cause)
22+
}
23+
24+
override getSuggestedActions(): string[] {
25+
const actions = [
26+
"Check configuration file syntax",
27+
"Verify required settings are present",
28+
"Reset to default configuration",
29+
]
30+
31+
if (this.configPath) {
32+
actions.push(`Check configuration file: ${this.configPath}`)
33+
}
34+
35+
if (this.configKey) {
36+
actions.push(`Verify configuration key "${this.configKey}" is correct`)
37+
}
38+
39+
return actions
40+
}
41+
42+
override getDocumentationLinks(): string[] {
43+
return [
44+
"https://docs.npmjs.com/cli/v8/configuring-npm/npmrc",
45+
"https://nodejs.org/api/fs.html#fs_fs_readfilesync_path_options",
46+
]
47+
}
48+
49+
override getUserFriendlyMessage(): string {
50+
if (this.configPath && this.configKey) {
51+
return `Configuration error in "${this.configPath}" for key "${this.configKey}": ${this.message}`
52+
}
53+
if (this.configPath) {
54+
return `Configuration error in "${this.configPath}": ${this.message}`
55+
}
56+
return `Configuration error: ${this.message}`
57+
}
58+
}
59+
60+
// Specific configuration error types
61+
export class InvalidConfigSyntaxError extends ConfigurationError {
62+
constructor(configPath: string, line?: number, context?: ErrorContext, cause?: Error) {
63+
const message = line
64+
? `Invalid syntax in configuration file at line ${line}`
65+
: "Invalid syntax in configuration file"
66+
67+
super(message, "CONFIG_INVALID_SYNTAX", configPath, undefined, context, cause)
68+
}
69+
70+
override getSuggestedActions(): string[] {
71+
return [
72+
"Check JSON/YAML syntax in configuration file",
73+
"Validate configuration with online JSON/YAML validator",
74+
"Check for missing commas, brackets, or quotes",
75+
"Reset configuration to default values",
76+
]
77+
}
78+
}
79+
80+
export class MissingConfigError extends ConfigurationError {
81+
constructor(configPath: string, context?: ErrorContext, cause?: Error) {
82+
super(`Configuration file not found: ${configPath}`, "CONFIG_NOT_FOUND", configPath, undefined, context, cause)
83+
}
84+
85+
override getSuggestedActions(): string[] {
86+
return [
87+
`Create configuration file at: ${this.configPath}`,
88+
"Run initialization command to create default config",
89+
"Check if configuration file path is correct",
90+
"Use --config flag to specify configuration file location",
91+
]
92+
}
93+
}
94+
95+
export class InvalidConfigValueError extends ConfigurationError {
96+
constructor(
97+
configKey: string,
98+
expectedType: string,
99+
actualValue: any,
100+
configPath?: string,
101+
context?: ErrorContext,
102+
cause?: Error,
103+
) {
104+
super(
105+
`Invalid value for "${configKey}": expected ${expectedType}, got ${typeof actualValue}`,
106+
"CONFIG_INVALID_VALUE",
107+
configPath,
108+
configKey,
109+
context,
110+
cause,
111+
)
112+
}
113+
114+
override getSuggestedActions(): string[] {
115+
return [
116+
`Check the value type for configuration key "${this.configKey}"`,
117+
"Refer to documentation for valid configuration values",
118+
"Use configuration validation tool",
119+
"Reset this configuration value to default",
120+
]
121+
}
122+
}
123+
124+
export class MissingRequiredConfigError extends ConfigurationError {
125+
constructor(configKey: string, configPath?: string, context?: ErrorContext, cause?: Error) {
126+
super(
127+
`Required configuration key "${configKey}" is missing`,
128+
"CONFIG_MISSING_REQUIRED",
129+
configPath,
130+
configKey,
131+
context,
132+
cause,
133+
)
134+
}
135+
136+
override getSuggestedActions(): string[] {
137+
return [
138+
`Add required configuration key "${this.configKey}"`,
139+
"Check documentation for required configuration options",
140+
"Run setup command to configure required settings",
141+
"Use environment variables as alternative configuration",
142+
]
143+
}
144+
}

src/cli/errors/FileSystemError.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* File system related errors
3+
*/
4+
5+
import { ErrorCategory, ErrorSeverity, ErrorContext } from "../types/error-types"
6+
import { CLIError } from "./CLIError"
7+
8+
export class FileSystemError extends CLIError {
9+
readonly category = ErrorCategory.FILE_SYSTEM
10+
readonly severity = ErrorSeverity.HIGH
11+
readonly isRecoverable = true
12+
13+
constructor(
14+
message: string,
15+
code: string,
16+
public readonly path?: string,
17+
public readonly operation?: string,
18+
context?: ErrorContext,
19+
cause?: Error,
20+
) {
21+
super(message, code, context, cause)
22+
}
23+
24+
override getSuggestedActions(): string[] {
25+
const actions = ["Check file permissions", "Verify file path exists", "Ensure sufficient disk space"]
26+
27+
if (this.path) {
28+
actions.push(`Verify path "${this.path}" is accessible`)
29+
}
30+
31+
if (this.operation === "write") {
32+
actions.push("Check if file is locked by another process")
33+
}
34+
35+
if (this.operation === "read") {
36+
actions.push("Ensure file exists and is readable")
37+
}
38+
39+
return actions
40+
}
41+
42+
override getDocumentationLinks(): string[] {
43+
return ["https://nodejs.org/api/fs.html", "https://docs.npmjs.com/cli/v8/commands/npm-config#files"]
44+
}
45+
46+
override getUserFriendlyMessage(): string {
47+
if (this.path && this.operation) {
48+
return `Failed to ${this.operation} file "${this.path}": ${this.message}`
49+
}
50+
return `File system error: ${this.message}`
51+
}
52+
}
53+
54+
// Specific file system error types
55+
export class FileNotFoundError extends FileSystemError {
56+
constructor(path: string, context?: ErrorContext, cause?: Error) {
57+
super(`File not found: ${path}`, "FS_FILE_NOT_FOUND", path, "read", context, cause)
58+
}
59+
60+
override getSuggestedActions(): string[] {
61+
return [
62+
`Check if file "${this.path}" exists`,
63+
"Verify the file path is correct",
64+
"Ensure you have read permissions for the directory",
65+
]
66+
}
67+
}
68+
69+
export class PermissionDeniedError extends FileSystemError {
70+
constructor(path: string, operation: string, context?: ErrorContext, cause?: Error) {
71+
super(`Permission denied: cannot ${operation} ${path}`, "FS_PERMISSION_DENIED", path, operation, context, cause)
72+
}
73+
74+
override getSuggestedActions(): string[] {
75+
return [
76+
`Check permissions for "${this.path}"`,
77+
"Run with appropriate privileges if needed",
78+
"Ensure you own the file or have necessary permissions",
79+
]
80+
}
81+
}
82+
83+
export class DiskSpaceError extends FileSystemError {
84+
constructor(path: string, context?: ErrorContext, cause?: Error) {
85+
super(`Insufficient disk space to write to ${path}`, "FS_DISK_SPACE", path, "write", context, cause)
86+
}
87+
88+
override getSuggestedActions(): string[] {
89+
return ["Free up disk space", "Choose a different location with more space", "Clean up temporary files"]
90+
}
91+
}

0 commit comments

Comments
 (0)